@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,201 @@
1
+ // O teammate ocioso e os leases (RM-059, fatia F10, T10.3). Módulo puro: a colisão de leases e de `touch_paths` pela
2
+ // regra do núcleo (`leasesColidem` e `globsColidem` de `core/src/leases.ts`, reimplementados aqui porque mod não
3
+ // importa código do núcleo, e cruzados no teste do núcleo), os leases ativos de todas as threads página a página do
4
+ // retrato do `ork maestro --json`, a escolha da próxima tarefa para o teammate, os tetos do piloto de agent teams e os
5
+ // textos. O Portão não toma nem solta lease (D8 revista): o `ork_git_commit` toma o `path:` de cada arquivo na hora do
6
+ // commit e recusa com qualquer lease no caminho, até da própria thread. As chamadas ao `$` ficam no register.ts.
7
+ import type { PortaoQuadro, PortaoTarefa } from '../types'
8
+
9
+ /** Os tetos do piloto (D10 do plano da thread), como o núcleo os passa no `env` do `--settings`. */
10
+ export const ENV_TEAMMATES = 'ORK_AGENT_TEAMS_TEAMMATES'
11
+ export const ENV_CUSTO = 'ORK_AGENT_TEAMS_CUSTO_USD'
12
+ export const TETO_MAXIMO_DE_TEAMMATES = 8
13
+ export const TETO_MAXIMO_DE_CUSTO_USD = 500
14
+ /**
15
+ * Quantas páginas da seção `leases` o Portão lê numa vez. O retrato traz até 50 itens por página e corta a página pelo
16
+ * teto de bytes do retrato inteiro (`maestro.page.byte_limit`), então uma página costuma vir com menos.
17
+ */
18
+ export const TETO_DE_PAGINAS_DE_LEASES = 10
19
+ /** A lacuna que o núcleo põe na página cortada pelo teto de bytes: é paginação, com `nextOffset`, não falta de dado. */
20
+ export const LACUNA_DE_PAGINA = 'maestro.page.byte_limit'
21
+ /** Os estados de agente que contam como teammate vivo para o teto. */
22
+ export const VIVOS: readonly string[] = ['pending', 'running', 'waiting', 'idle']
23
+
24
+ const NOME_DE_TEAMMATE = /^[A-Za-z0-9][A-Za-z0-9._@-]{0,79}$/
25
+ const FAMILIAS = ['worktree-write', 'path', 'board', 'service', 'exec']
26
+
27
+ /** A família de um lease, como `tipoDoLease` do núcleo: prefixo conhecido, `main-tree`, ou `path` por padrão. */
28
+ export function familiaDoLease(nome: string): string {
29
+ if (nome === 'main-tree') return 'main-tree'
30
+ const prefixo = nome.split(':')[0] as string
31
+ return FAMILIAS.includes(prefixo) ? prefixo : 'path'
32
+ }
33
+
34
+ /** O alvo de um lease (o que vem depois dos dois pontos), como `alvoDoLease` do núcleo. */
35
+ export function alvoDoLease(nome: string): string {
36
+ const corte = nome.indexOf(':')
37
+ return corte < 0 ? '' : nome.slice(corte + 1)
38
+ }
39
+
40
+ /** O glob de caminho (`*`, `**`, `?`) como a regex equivalente, como `globParaRegex` do núcleo. */
41
+ export function globParaRegex(glob: string): RegExp {
42
+ let re = ''
43
+ for (let i = 0; i < glob.length; i++) {
44
+ const c = glob[i] as string
45
+ if (c === '*') {
46
+ if (glob[i + 1] === '*') {
47
+ re += '.*'
48
+ i++
49
+ if (glob[i + 1] === '/') i++
50
+ } else {
51
+ re += '[^/]*'
52
+ }
53
+ } else if (c === '?') {
54
+ re += '[^/]'
55
+ } else {
56
+ re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&')
57
+ }
58
+ }
59
+ return new RegExp(`^${re}$`)
60
+ }
61
+
62
+ function prefixoLiteral(glob: string): string {
63
+ const corte = glob.search(/[*?]/)
64
+ return corte < 0 ? glob : glob.slice(0, corte)
65
+ }
66
+
67
+ function prefixoDeCaminho(a: string, b: string): boolean {
68
+ if (a === '') return false
69
+ if (!b.startsWith(a)) return false
70
+ return a.length === b.length || a.endsWith('/') || b[a.length] === '/'
71
+ }
72
+
73
+ /** Dois globs disputam a mesma região? Conservador como o do núcleo: na dúvida, colide. */
74
+ export function globsColidem(a: string, b: string): boolean {
75
+ if (a === b) return true
76
+ if (globParaRegex(a).test(b) || globParaRegex(b).test(a)) return true
77
+ const pa = prefixoLiteral(a)
78
+ const pb = prefixoLiteral(b)
79
+ return prefixoDeCaminho(pa, pb) || prefixoDeCaminho(pb, pa)
80
+ }
81
+
82
+ /** Dois nomes de lease colidem? Famílias diferentes nunca; `path` pela colisão de glob; o resto pela igualdade. */
83
+ export function leasesColidem(a: string, b: string): boolean {
84
+ if (a === b) return true
85
+ const fa = familiaDoLease(a)
86
+ if (fa !== familiaDoLease(b)) return false
87
+ return fa === 'path' ? globsColidem(alvoDoLease(a), alvoDoLease(b)) : false
88
+ }
89
+
90
+ /** O lease `path:` de um `touch_path` da tarefa. */
91
+ export function nomeDoLease(touchPath: string): string {
92
+ return `path:${touchPath}`
93
+ }
94
+
95
+ /** Um lease ativo do retrato: o nome e a thread que o segura. */
96
+ export type LeaseAtivo = { nome: string; thread: string }
97
+
98
+ /** Os leases ativos de uma página da seção `leases` do retrato `ork.maestro-snapshot/v1`, de qualquer família. */
99
+ export function leasesDoRetrato(retrato: unknown): LeaseAtivo[] {
100
+ const itens = (retrato as { sections?: { leases?: { items?: unknown } } } | null)?.sections?.leases?.items
101
+ if (!Array.isArray(itens)) throw new Error('ork-portao: o retrato veio sem a seção leases')
102
+ const ativos: LeaseAtivo[] = []
103
+ for (const bruto of itens) {
104
+ const item = bruto as { id?: unknown; status?: unknown; refs?: unknown } | null
105
+ if (!item || typeof item.id !== 'string' || item.status !== 'active' || !Array.isArray(item.refs)) continue
106
+ const ref = (item.refs as Array<{ kind?: unknown; id?: unknown } | null>).find(r => r?.kind === 'thread')
107
+ if (ref && typeof ref.id === 'string') ativos.push({ nome: item.id, thread: ref.id })
108
+ }
109
+ return ativos
110
+ }
111
+
112
+ /** Uma página da seção `leases`: os ativos, o offset da página seguinte (`coverage.nextOffset`) e o que faltou. */
113
+ export type PaginaDeLeases = { leases: LeaseAtivo[]; proximo: number | null; lacuna: string | null }
114
+
115
+ /**
116
+ * A página da seção `leases` de um retrato. Seção indisponível lança (sem ela, nenhuma região está livre). A página
117
+ * cortada pelo teto de bytes (`maestro.page.byte_limit`) só diz que há página seguinte; qualquer outra lacuna, ou o
118
+ * retrato em conflito (fontes mudando durante a leitura), deixa a leitura incompleta.
119
+ */
120
+ export function paginaDosLeases(retrato: unknown): PaginaDeLeases {
121
+ type Secao = { state?: unknown; gaps?: unknown; coverage?: { nextOffset?: unknown } }
122
+ const secao = (retrato as { sections?: { leases?: Secao } } | null)?.sections?.leases
123
+ if (!secao || typeof secao !== 'object') throw new Error('ork-portao: o retrato veio sem a seção leases')
124
+ const faltas = Array.isArray(secao.gaps) ? secao.gaps.filter((g): g is string => typeof g === 'string') : []
125
+ if (secao.state === 'unavailable') throw new Error(`ork-portao: a seção leases veio indisponível${faltas.length ? ` (${faltas.join(', ')})` : ''}`)
126
+ const seguinte = secao.coverage?.nextOffset
127
+ const proximo = typeof seguinte === 'number' && Number.isInteger(seguinte) && seguinte > 0 ? seguinte : null
128
+ const outras = faltas.filter(g => g !== LACUNA_DE_PAGINA)
129
+ const lacuna = secao.state === 'conflict' ? 'a seção leases veio em conflito (o retrato mudou durante a leitura)'
130
+ : outras.length > 0 ? `a seção leases veio com lacunas (${outras.join(', ')})` : null
131
+ return { leases: leasesDoRetrato(retrato), proximo, lacuna }
132
+ }
133
+
134
+ /** Os tetos do piloto lidos do ambiente: teammates inteiro de 1 a 8 e custo de 0 a 500 dólares; `null` sem os dois. */
135
+ export function tetosDoAmbiente(teammates: unknown, custo: unknown): { teammates: number; custoUsd: number } | null {
136
+ if (typeof teammates !== 'string' || typeof custo !== 'string') return null
137
+ const t = /^\d+$/.test(teammates.trim()) ? Number(teammates.trim()) : NaN
138
+ const c = /^\d+(?:\.\d+)?$/.test(custo.trim()) ? Number(custo.trim()) : NaN
139
+ if (!Number.isInteger(t) || t < 1 || t > TETO_MAXIMO_DE_TEAMMATES) return null
140
+ if (!Number.isFinite(c) || c <= 0 || c > TETO_MAXIMO_DE_CUSTO_USD) return null
141
+ return { teammates: t, custoUsd: c }
142
+ }
143
+
144
+ const dolares = (v: number) => `US$ ${v.toFixed(2)}`
145
+
146
+ /** Por que o `agent.spawn` de um teammate é recusado (D10); `null` quando ele passa. */
147
+ export function recusaDoTeammate(tetos: { teammates: number; custoUsd: number } | null, vivos: number, custo: number | null): string | null {
148
+ if (!tetos) {
149
+ return 'Portão de Tarefas: teammate recusado. Na sessão de fase, agent teams só rodam no piloto do núcleo, com os tetos de ' +
150
+ `runtime.agent_teams no manifesto, e esta sessão não recebeu ${ENV_TEAMMATES} e ${ENV_CUSTO}.`
151
+ }
152
+ if (vivos >= tetos.teammates) return `Portão de Tarefas: teammate recusado. A sessão já tem ${vivos} teammate(s) vivo(s), e o teto do piloto é ${tetos.teammates}.`
153
+ if (custo === null) return 'Portão de Tarefas: teammate recusado. O custo da sessão não pôde ser lido, e sem ele o teto de custo do piloto não vale.'
154
+ if (custo >= tetos.custoUsd) return `Portão de Tarefas: teammate recusado. O custo da sessão (${dolares(custo)}) chegou ao teto do piloto (${dolares(tetos.custoUsd)}).`
155
+ return null
156
+ }
157
+
158
+ /** O nome do teammate como o evento traz, conferido; `null` quando não serve. */
159
+ export function nomeDoTeammate(bruto: unknown): string | null {
160
+ return typeof bruto === 'string' && NOME_DE_TEAMMATE.test(bruto) ? bruto : null
161
+ }
162
+
163
+ /**
164
+ * A próxima tarefa para o teammate (D7 e D8 revista): na ordem do plano, a que serve ao quadro, não fechou, não foi
165
+ * devolvida, não está com outro teammate, não cruza os `touch_paths` das tarefas com outros teammates e não cruza lease
166
+ * `path:` ativo de nenhuma thread, a própria incluída, porque o `ork_git_commit` recusa o commit com qualquer lease no
167
+ * caminho. Sem tarefa livre, `null`.
168
+ */
169
+ export function escolherTarefa(entrada: {
170
+ tarefas: readonly PortaoTarefa[]
171
+ quadro: PortaoQuadro
172
+ teammate: string
173
+ leases: readonly LeaseAtivo[]
174
+ }): PortaoTarefa | null {
175
+ const { tarefas, quadro, teammate, leases } = entrada
176
+ const comOutros = Object.entries(quadro.atribuicoes)
177
+ .filter(([quem, tarefa]) => quem !== teammate && !quadro.concluidas.includes(tarefa))
178
+ .map(([, tarefa]) => tarefa)
179
+ const regiaoDosOutros = tarefas.filter(t => comOutros.includes(t.id)).flatMap(t => t.touchPaths)
180
+ for (const tarefa of tarefas) {
181
+ const livre = tarefa.touchPaths.length > 0 && tarefa.verify !== null && tarefa.problema === null &&
182
+ !quadro.concluidas.includes(tarefa.id) && !quadro.devolvidas.includes(tarefa.id) && !comOutros.includes(tarefa.id) &&
183
+ !tarefa.touchPaths.some(tp => regiaoDosOutros.some(outro => globsColidem(tp, outro))) &&
184
+ !tarefa.touchPaths.some(tp => leases.some(l => leasesColidem(nomeDoLease(tp), l.nome)))
185
+ if (livre) return tarefa
186
+ }
187
+ return null
188
+ }
189
+
190
+ /** O texto do `block` que entrega a tarefa ao teammate ocioso (D7). */
191
+ export function textoDaEntrega(t: PortaoTarefa, teammate: string): string {
192
+ return [
193
+ `Portão de Tarefas: ${teammate}, pegue a tarefa ${t.id} do plano${t.titulo ? ` (${t.titulo})` : ''}.`,
194
+ ` touch_paths: ${t.touchPaths.join(', ')}`,
195
+ ` verify: ${t.verify ?? ''}`,
196
+ 'Antes de começar, confira o quadro (TaskList): se ela já está em andamento com outro dono, não a pegue e fique ocioso. ' +
197
+ `Se ela não está no quadro, crie-a com o assunto começando por ${t.id}; marque-a em andamento com você como dono. ` +
198
+ 'Quando o Portão a escolheu, nenhum lease path: ativo cruzava essa região, e o commit pelo ork_git_commit toma o lease ' +
199
+ 'de cada arquivo na hora. Ela só fecha com o verify aprovado pela conferência do núcleo.',
200
+ ].join('\n')
201
+ }
@@ -0,0 +1,304 @@
1
+ // O plano da thread como o Portão de Tarefas o usa (RM-059, fatia F10, T10.1). Módulo puro: interpreta o que o
2
+ // `ork_artifact_read` devolve, acha as tarefas `T<id>` com os `touch_paths` e o verify de cada uma pela mesma regra
3
+ // do `ork claims conferir --tarefa` do núcleo, tira o id da tarefa do assunto da tarefa nativa, decide a criação e
4
+ // lê o escopo no retrato da ponte. As chamadas ao `$` ficam no register.ts; nada aqui lê `.orkastery/` do disco.
5
+ import type { PortaoTarefa } from '../types'
6
+
7
+ export const SERVIDOR = 'orkastery'
8
+ /**
9
+ * A lista fechada de ferramentas MCP do Portão: só a leitura do plano. O `$.mcp.call` não pede permissão ao
10
+ * dono, então o teste do núcleo confere pela AST que o mod só chama esta.
11
+ */
12
+ export const FERRAMENTA_DO_PLANO = 'ork_artifact_read'
13
+ export const FERRAMENTAS_DO_PORTAO: readonly string[] = [FERRAMENTA_DO_PLANO]
14
+ /** O plano lido vale por este tempo; sem plano (ou com erro), a leitura se repete depois de pouco. */
15
+ export const IDADE_DO_PLANO_MS = 60000
16
+ export const IDADE_SEM_PLANO_MS = 5000
17
+ /** Quantas tarefas do plano a recusa lista ao modelo, e o teto do texto inteiro. */
18
+ export const TETO_DE_TAREFAS_NO_TEXTO = 12
19
+ export const TETO_DO_TEXTO = 4000
20
+
21
+ const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
22
+ /** O cabeçalho de tarefa do núcleo (`verifyDaTarefa`): `##` a `######`, `T<id>`, e o resto da linha. */
23
+ const CABECALHO_DE_TAREFA = /^(#{2,6})\s+T(\d+(?:\.\d+)*)(?=[\s.:)]|$)(.*)$/
24
+ /** A linha de verify nos formatos que o núcleo aceita; o grupo 1 é o que vem depois do rótulo. */
25
+ const ROTULO_DE_VERIFY = /^\s*(?:[-*]\s+)?(?:\*\*verify:\*\*|\*\*verify\*\*\s*:|`verify`\s*:|verify\s*:)\s*(.*)$/i
26
+ /**
27
+ * O rótulo de `touch_paths` nos formatos em uso nos planos (o da carga do Freio), também com a contagem entre parênteses
28
+ * antes dos dois-pontos (`- touch_paths (4): ...`).
29
+ */
30
+ const ROTULO_DE_TOUCH_PATHS = /^\s*(?:[-*+]\s+)?(?:\*\*touch_paths(?:\s*\([^)]*\))?:?\*\*\s*:?|`touch_paths`(?:\s*\([^)]*\))?\s*:|touch_paths(?:\s*\([^)]*\))?\s*:)\s*(.*)$/i
31
+ /** Trecho entre crases com cara de caminho: barra, ponto ou curinga. Identificador da prosa (`Modo`, `fast`) não conta. */
32
+ const CARA_DE_CAMINHO = /[/.*?]/
33
+ /** Linha que abre outro item de lista, cabeçalho ou cerca de código. */
34
+ const ABRE_BLOCO = /^\s*(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|```|~~~)/
35
+ const TRECHO = /`([^`]+)`/g
36
+ /** O id de uma tarefa do plano como palavra própria no assunto (`T2.1 ...`, `[T3]`), nunca colado a letra. */
37
+ const ID_NO_ASSUNTO = /(?:^|[^A-Za-z0-9_])T(\d+(?:\.\d+)*)(?![A-Za-z0-9_]|\.\d)/g
38
+ /** As fases na ordem do núcleo, com o dígito que o slug do bloco usa (`DIGITO_DA_FASE`). */
39
+ const FASES = ['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER'] as const
40
+
41
+ export class ErroDoPlano extends Error {}
42
+
43
+ /** Os argumentos da leitura do plano da thread do despacho. */
44
+ export function argsDoPlano(thread: string): Record<string, unknown> {
45
+ if (!ID_DE_THREAD.test(thread)) throw new ErroDoPlano('ork-portao: id de thread inválido')
46
+ return { threadId: thread, tipo: 'plan' }
47
+ }
48
+
49
+ /** O JSON do primeiro bloco de texto de um resultado MCP; resultado com erro vira exceção. */
50
+ export function jsonDoMcp(r: unknown, ferramenta: string): unknown {
51
+ const resultado = r as { content?: ReadonlyArray<{ type?: string; text?: unknown }>; isError?: boolean } | null
52
+ const texto = (resultado?.content ?? []).find(c => c?.type === 'text' && typeof c.text === 'string')?.text as string | undefined
53
+ if (resultado?.isError) throw new ErroDoPlano(`ork-portao: ${ferramenta} respondeu com erro${texto ? `: ${texto.slice(0, 200)}` : ''}`)
54
+ if (texto === undefined) throw new ErroDoPlano(`ork-portao: ${ferramenta} não devolveu texto`)
55
+ try {
56
+ return JSON.parse(texto)
57
+ } catch {
58
+ throw new ErroDoPlano(`ork-portao: ${ferramenta} devolveu texto que não é JSON`)
59
+ }
60
+ }
61
+
62
+ /** O texto do plano que o `ork_artifact_read` devolve, ou `null` quando a thread ainda não tem plano. */
63
+ export function textoDoPlano(bruto: unknown): string | null {
64
+ const documento = bruto as { conteudo?: unknown } | null
65
+ if (!documento || typeof documento !== 'object') throw new ErroDoPlano('ork-portao: o plano lido não é objeto')
66
+ return typeof documento.conteudo === 'string' ? documento.conteudo : null
67
+ }
68
+
69
+ /** Caminho relativo normalizado: barras para a frente, sem `./` na frente nem barra no fim. */
70
+ export function normalizar(caminho: string): string {
71
+ return caminho.trim().replace(/\\/g, '/').replace(/^(?:\.\/)+/, '').replace(/\/+$/, '')
72
+ }
73
+
74
+ function ehCaminho(texto: string): boolean {
75
+ return texto !== '' && !/\s/.test(texto) && !texto.startsWith('-') && !texto.split('/').includes('..')
76
+ }
77
+
78
+ const recuo = (linha: string) => (/^[ \t]*/.exec(linha)?.[0] ?? '').replace(/\t/g, ' ').length
79
+
80
+ /**
81
+ * O item inteiro do verify da linha `k` (a regra de `itemDoVerify` do núcleo): o texto depois do rótulo, a
82
+ * continuação logo abaixo que não abre outro item e as linhas mais recuadas que o rótulo, mesmo depois de branco.
83
+ */
84
+ function itemDoVerify(bloco: readonly string[], k: number, inicio: string): string {
85
+ const base = recuo(bloco[k] as string)
86
+ const partes = [inicio]
87
+ let depoisDeBranco = false
88
+ for (const linha of bloco.slice(k + 1)) {
89
+ if (!linha.trim()) { depoisDeBranco = true; continue }
90
+ if (recuo(linha) <= base && (depoisDeBranco || ABRE_BLOCO.test(linha))) break
91
+ partes.push(linha.trim())
92
+ depoisDeBranco = false
93
+ }
94
+ return partes.join(' ').trim()
95
+ }
96
+
97
+ /** O verify de uma tarefa pela regra do núcleo: o comando, ou o motivo com que o `ork claims conferir` recusaria. */
98
+ function verifyDoBloco(bloco: readonly string[]): { verify: string | null; problema: PortaoTarefa['problema'] } {
99
+ const verifies = bloco.flatMap((l, k) => {
100
+ const m = ROTULO_DE_VERIFY.exec(l)
101
+ return m ? [itemDoVerify(bloco, k, (m[1] as string).trim())] : []
102
+ })
103
+ if (verifies.length === 0) return { verify: null, problema: 'tarefa-sem-verify' }
104
+ if (verifies.length > 1) return { verify: null, problema: 'tarefa-ambigua' }
105
+ const resto = verifies[0] as string
106
+ if ([...resto.matchAll(TRECHO)].length === 0) return { verify: null, problema: 'tarefa-sem-comando' }
107
+ const unico = /^`([^`]+)`\s*\.?$/.exec(resto)
108
+ if (!unico || !(unico[1] as string).trim()) return { verify: null, problema: 'tarefa-ambigua' }
109
+ return { verify: (unico[1] as string).trim(), problema: null }
110
+ }
111
+
112
+ /** Os `touch_paths` de uma tarefa: os trechos entre crases do item do rótulo e das linhas que o continuam. */
113
+ function touchPathsDoBloco(bloco: readonly string[]): string[] {
114
+ const achados: string[] = []
115
+ const juntar = (texto: string) => {
116
+ for (const m of texto.matchAll(TRECHO)) {
117
+ const bruto = (m[1] as string).trim()
118
+ if (!CARA_DE_CAMINHO.test(bruto)) continue
119
+ const caminho = normalizar(bruto)
120
+ if (ehCaminho(caminho) && !achados.includes(caminho)) achados.push(caminho)
121
+ }
122
+ }
123
+ let recuoDoRotulo = -1
124
+ let depoisDeBranco = false
125
+ for (const linha of bloco) {
126
+ const r = recuo(linha)
127
+ const rotulo = ROTULO_DE_TOUCH_PATHS.exec(linha)
128
+ if (rotulo) {
129
+ juntar(rotulo[1] as string)
130
+ recuoDoRotulo = r
131
+ depoisDeBranco = false
132
+ continue
133
+ }
134
+ if (recuoDoRotulo < 0) continue
135
+ if (linha.trim() === '') { depoisDeBranco = true; continue }
136
+ if (r <= recuoDoRotulo && (depoisDeBranco || ABRE_BLOCO.test(linha))) { recuoDoRotulo = -1; continue }
137
+ juntar(linha)
138
+ depoisDeBranco = false
139
+ }
140
+ return achados
141
+ }
142
+
143
+ /**
144
+ * As tarefas do plano, na ordem do texto (D3 do plano da thread): o cabeçalho `##` a `######` com `T<id>`, o bloco
145
+ * até o próximo cabeçalho de nível igual ou maior, os `touch_paths` e o verify pela regra do núcleo. Id repetido
146
+ * fica uma vez só, com o problema `tarefa-repetida`. `null` sem plano.
147
+ */
148
+ export function tarefasDoPlano(conteudo: string | null): PortaoTarefa[] | null {
149
+ if (typeof conteudo !== 'string') return null
150
+ const linhas = conteudo.replace(/\r\n/g, '\n').split('\n')
151
+ const inicios = linhas.flatMap((l, i) => {
152
+ const m = CABECALHO_DE_TAREFA.exec(l)
153
+ return m ? [{ i, nivel: (m[1] as string).length, id: `T${m[2] as string}`, titulo: (m[3] as string).replace(/^[\s.:)-]+/, '').trim() }] : []
154
+ })
155
+ const vistos = new Map<string, number>()
156
+ for (const c of inicios) vistos.set(c.id, (vistos.get(c.id) ?? 0) + 1)
157
+ const tarefas: PortaoTarefa[] = []
158
+ for (const c of inicios) {
159
+ if (tarefas.some(t => t.id === c.id)) continue
160
+ const fim = linhas.findIndex((l, j) => j > c.i && new RegExp(`^#{1,${c.nivel}}\\s`).test(l))
161
+ const bloco = linhas.slice(c.i + 1, fim < 0 ? undefined : fim)
162
+ const { verify, problema } = (vistos.get(c.id) ?? 0) > 1 ? { verify: null, problema: 'tarefa-repetida' as const } : verifyDoBloco(bloco)
163
+ tarefas.push({ id: c.id, titulo: c.titulo.slice(0, 120), touchPaths: touchPathsDoBloco(bloco), verify, problema })
164
+ }
165
+ return tarefas
166
+ }
167
+
168
+ /** Os ids distintos de tarefa do plano que o assunto da tarefa nativa cita, na ordem. */
169
+ export function idsNoAssunto(assunto: unknown): string[] {
170
+ if (typeof assunto !== 'string') return []
171
+ const ids: string[] = []
172
+ for (const m of assunto.matchAll(ID_NO_ASSUNTO)) {
173
+ const id = `T${m[1] as string}`
174
+ if (!ids.includes(id)) ids.push(id)
175
+ }
176
+ return ids
177
+ }
178
+
179
+ /** A tarefa serve para o quadro: tem `touch_paths` e um verify que o núcleo confere. */
180
+ export function serveAoQuadro(t: PortaoTarefa): boolean {
181
+ return t.touchPaths.length > 0 && t.verify !== null && t.problema === null
182
+ }
183
+
184
+ /** Por que a criação foi recusada (D2 e D3 do plano da thread). */
185
+ export type MotivoDaRecusa = 'sem-id' | 'ambigua' | 'fora-do-plano' | 'repetida' | 'sem-touch-paths' | 'sem-verify'
186
+
187
+ export type VereditoDaCriacao =
188
+ | { tipo: 'passa'; tarefa: string }
189
+ | { tipo: 'recusa'; motivo: MotivoDaRecusa; ids: string[]; tarefa: PortaoTarefa | null }
190
+
191
+ /** A criação de uma tarefa nativa contra as tarefas do plano. */
192
+ export function decidirCriacao(assunto: unknown, tarefas: readonly PortaoTarefa[]): VereditoDaCriacao {
193
+ const ids = idsNoAssunto(assunto)
194
+ if (ids.length === 0) return { tipo: 'recusa', motivo: 'sem-id', ids, tarefa: null }
195
+ if (ids.length > 1) return { tipo: 'recusa', motivo: 'ambigua', ids, tarefa: null }
196
+ const tarefa = tarefas.find(t => t.id === ids[0]) ?? null
197
+ if (!tarefa) return { tipo: 'recusa', motivo: 'fora-do-plano', ids, tarefa: null }
198
+ if (tarefa.problema === 'tarefa-repetida') return { tipo: 'recusa', motivo: 'repetida', ids, tarefa }
199
+ if (tarefa.touchPaths.length === 0) return { tipo: 'recusa', motivo: 'sem-touch-paths', ids, tarefa }
200
+ if (!serveAoQuadro(tarefa)) return { tipo: 'recusa', motivo: 'sem-verify', ids, tarefa }
201
+ return { tipo: 'passa', tarefa: tarefa.id }
202
+ }
203
+
204
+ const DETALHE_DO_VERIFY: Record<string, string> = {
205
+ 'tarefa-sem-verify': 'não há linha de verify',
206
+ 'tarefa-sem-comando': 'o verify não tem comando entre crases',
207
+ 'tarefa-ambigua': 'o verify tem mais de uma linha, mais de um comando ou texto além do comando',
208
+ }
209
+
210
+ function motivoEmTexto(v: Extract<VereditoDaCriacao, { tipo: 'recusa' }>): string {
211
+ const id = v.ids[0] ?? ''
212
+ switch (v.motivo) {
213
+ case 'sem-id': return 'o assunto não traz o id de uma tarefa do plano'
214
+ case 'ambigua': return `o assunto cita mais de uma tarefa do plano (${v.ids.join(', ')}), e cada tarefa do quadro é uma tarefa do plano`
215
+ case 'fora-do-plano': return `o plano não tem a tarefa ${id}`
216
+ case 'repetida': return `o plano tem mais de um cabeçalho da tarefa ${id}`
217
+ case 'sem-touch-paths': return `a tarefa ${id} do plano não tem touch_paths (caminhos entre crases, com barra, ponto ou curinga), então ninguém sabe que arquivos ela toca`
218
+ case 'sem-verify': return `a tarefa ${id} do plano não tem um verify que o núcleo confira (${DETALHE_DO_VERIFY[v.tarefa?.problema ?? ''] ?? 'sem comando'})`
219
+ }
220
+ }
221
+
222
+ /** Uma tarefa do plano como a lista ao modelo: o id e o título. */
223
+ export function rotulo(t: PortaoTarefa): string {
224
+ return t.titulo ? `${t.id} (${t.titulo})` : t.id
225
+ }
226
+
227
+ /** O texto do `block` da criação recusada: o motivo, a regra e as tarefas do plano que servem. */
228
+ export function textoDaRecusa(v: Extract<VereditoDaCriacao, { tipo: 'recusa' }>, assunto: unknown, tarefas: readonly PortaoTarefa[]): string {
229
+ const nome = typeof assunto === 'string' ? assunto.trim().slice(0, 120) : ''
230
+ const servem = tarefas.filter(serveAoQuadro)
231
+ const partes = [
232
+ `Portão de Tarefas: a tarefa "${nome}" não entrou no quadro: ${motivoEmTexto(v)}.`,
233
+ 'No GO desta thread, cada tarefa do quadro é uma tarefa do plano: o assunto leva o id dela (por exemplo, "T2.1 título"), ' +
234
+ 'e a tarefa do plano precisa de touch_paths e de um verify com um comando só entre crases, que o núcleo confere ' +
235
+ 'quando a tarefa fecha (ork claims conferir --tarefa).',
236
+ ]
237
+ if (servem.length === 0) partes.push('Nenhuma tarefa do plano serve hoje: o plano precisa de touch_paths e de verify nas tarefas antes de abrir o quadro.')
238
+ else {
239
+ const lista = servem.slice(0, TETO_DE_TAREFAS_NO_TEXTO).map(rotulo).join('; ')
240
+ const mais = servem.length > TETO_DE_TAREFAS_NO_TEXTO ? `; e mais ${servem.length - TETO_DE_TAREFAS_NO_TEXTO}` : ''
241
+ partes.push(`Tarefas do plano que servem: ${lista}${mais}.`)
242
+ }
243
+ return limitarTexto(partes.join('\n'))
244
+ }
245
+
246
+ /** Um texto ao modelo dentro do teto, com a marca de corte. */
247
+ export function limitarTexto(texto: string): string {
248
+ return texto.length > TETO_DO_TEXTO ? `${texto.slice(0, TETO_DO_TEXTO - 40)}\n(texto cortado pelo Portão de Tarefas)` : texto
249
+ }
250
+
251
+ /**
252
+ * O GO está no bloco pelo slug que o núcleo dá a ele (`slugDasFases`): `full` são todas as fases, o nome de uma
253
+ * fase em minúsculas é ela só, e `f` seguido de dígitos são as fases desses dígitos. `null` quando o slug não diz.
254
+ */
255
+ export function faseNoBloco(slug: unknown, fase: string = 'GO'): boolean | null {
256
+ if (typeof slug !== 'string' || slug === '') return null
257
+ if (slug === 'full') return true
258
+ const so = FASES.find(f => f.toLowerCase() === slug)
259
+ if (so) return so === fase
260
+ if (!/^f[1-6]+$/.test(slug)) return null
261
+ const digito = String(FASES.indexOf(fase as (typeof FASES)[number]) + 1)
262
+ return slug.slice(1).includes(digito)
263
+ }
264
+
265
+ /** O status e o slug do bloco da thread no retrato `ork.maestro-snapshot/v1`; `null` no que ele não diz. */
266
+ export function threadNoRetrato(retrato: unknown, thread: string): { status: string | null; bloco: string | null } {
267
+ const itens = (retrato as { sections?: { threads?: { items?: unknown } } } | null)?.sections?.threads?.items
268
+ if (!Array.isArray(itens)) return { status: null, bloco: null }
269
+ const item = itens.find(i => (i as { id?: unknown } | null)?.id === thread) as { status?: unknown; facts?: { block?: unknown } } | undefined
270
+ return {
271
+ status: typeof item?.status === 'string' ? item.status : null,
272
+ bloco: typeof item?.facts?.block === 'string' ? item.facts.block : null,
273
+ }
274
+ }
275
+
276
+ /** O que decide se o Portão age (D1). */
277
+ export type Escopo = { ativo: true; avisos: string[] } | { ativo: false; motivo: string }
278
+
279
+ /** A metade do escopo que o retrato decide, antes de ler o plano: sem retrato, conta como bloco com GO e aberta. */
280
+ export function escopoDoRetrato(retrato: { status: string | null; bloco: string | null } | null): Escopo {
281
+ if (retrato === null) return { ativo: true, avisos: ['retrato indisponível; o Portão confere mesmo assim'] }
282
+ if (retrato.status === 'fechada') return { ativo: false, motivo: 'thread fechada' }
283
+ if (faseNoBloco(retrato.bloco) === false) return { ativo: false, motivo: `o bloco ${retrato.bloco} não tem o GO` }
284
+ return { ativo: true, avisos: [] }
285
+ }
286
+
287
+ /**
288
+ * A outra metade (D1 refinada): o plano legível com ao menos uma tarefa que serve ao quadro. Plano em que nenhuma tarefa
289
+ * tem `touch_paths` e verify conferível não ganha portão: travaria o quadro inteiro do GO.
290
+ */
291
+ export function escopoDoPlano(tarefas: readonly PortaoTarefa[] | null): Escopo {
292
+ if (tarefas === null) return { ativo: false, motivo: 'a thread ainda não tem plano legível' }
293
+ if (tarefas.length === 0) return { ativo: false, motivo: 'o plano não tem tarefas T<n>' }
294
+ if (!tarefas.some(serveAoQuadro)) return { ativo: false, motivo: 'nenhuma tarefa do plano tem touch_paths e verify conferível' }
295
+ return { ativo: true, avisos: [] }
296
+ }
297
+
298
+ /** O escopo inteiro, como o register.ts o compõe: o retrato primeiro, o plano depois. */
299
+ export function decidirEscopo(retrato: { status: string | null; bloco: string | null } | null, tarefas: readonly PortaoTarefa[] | null): Escopo {
300
+ const doRetrato = escopoDoRetrato(retrato)
301
+ if (!doRetrato.ativo) return doRetrato
302
+ const doPlano = escopoDoPlano(tarefas)
303
+ return doPlano.ativo ? doRetrato : doPlano
304
+ }