@orkastery/cli 0.5.3 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/adapters/claude-code/README.md +158 -0
  2. package/adapters/claude-code/mods/autoridade.json +56 -0
  3. package/adapters/claude-code/mods/ork-clima/.claude-plugin/plugin.json +14 -0
  4. package/adapters/claude-code/mods/ork-clima/hooks/contas.ts +283 -0
  5. package/adapters/claude-code/mods/ork-clima/hooks/hooks.json +3 -0
  6. package/adapters/claude-code/mods/ork-clima/hooks/previsao.ts +393 -0
  7. package/adapters/claude-code/mods/ork-clima/hooks/register.ts +205 -0
  8. package/adapters/claude-code/mods/ork-clima/orkastery-mod.json +5 -0
  9. package/adapters/claude-code/mods/ork-clima/types/index.d.ts +80 -0
  10. package/adapters/claude-code/mods/ork-freio/.claude-plugin/plugin.json +14 -0
  11. package/adapters/claude-code/mods/ork-freio/hooks/carga.ts +160 -0
  12. package/adapters/claude-code/mods/ork-freio/hooks/conferencia.ts +309 -0
  13. package/adapters/claude-code/mods/ork-freio/hooks/hooks.json +3 -0
  14. package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +270 -0
  15. package/adapters/claude-code/mods/ork-freio/hooks/register.ts +285 -0
  16. package/adapters/claude-code/mods/ork-freio/orkastery-mod.json +5 -0
  17. package/adapters/claude-code/mods/ork-freio/types/index.d.ts +127 -0
  18. package/adapters/claude-code/mods/ork-mesa/.claude-plugin/plugin.json +14 -0
  19. package/adapters/claude-code/mods/ork-mesa/hooks/cartoes.tsx +246 -0
  20. package/adapters/claude-code/mods/ork-mesa/hooks/decidir.ts +110 -0
  21. package/adapters/claude-code/mods/ork-mesa/hooks/fila.ts +253 -0
  22. package/adapters/claude-code/mods/ork-mesa/hooks/hooks.json +3 -0
  23. package/adapters/claude-code/mods/ork-mesa/hooks/obvia.ts +98 -0
  24. package/adapters/claude-code/mods/ork-mesa/hooks/register.tsx +493 -0
  25. package/adapters/claude-code/mods/ork-mesa/orkastery-mod.json +5 -0
  26. package/adapters/claude-code/mods/ork-mesa/types/index.d.ts +131 -0
  27. package/adapters/claude-code/mods/ork-pista/.claude-plugin/plugin.json +14 -0
  28. package/adapters/claude-code/mods/ork-pista/hooks/acoes.ts +71 -0
  29. package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +117 -0
  30. package/adapters/claude-code/mods/ork-pista/hooks/hooks.json +3 -0
  31. package/adapters/claude-code/mods/ork-pista/hooks/pista.tsx +356 -0
  32. package/adapters/claude-code/mods/ork-pista/hooks/register.tsx +439 -0
  33. package/adapters/claude-code/mods/ork-pista/orkastery-mod.json +5 -0
  34. package/adapters/claude-code/mods/ork-pista/types/index.d.ts +85 -0
  35. package/adapters/claude-code/mods/ork-ponte/.claude-plugin/plugin.json +13 -0
  36. package/adapters/claude-code/mods/ork-ponte/hooks/faixa.tsx +57 -0
  37. package/adapters/claude-code/mods/ork-ponte/hooks/guarda.ts +410 -0
  38. package/adapters/claude-code/mods/ork-ponte/hooks/hooks.json +3 -0
  39. package/adapters/claude-code/mods/ork-ponte/hooks/mostrado.ts +55 -0
  40. package/adapters/claude-code/mods/ork-ponte/hooks/nucleo.ts +157 -0
  41. package/adapters/claude-code/mods/ork-ponte/hooks/papel.ts +42 -0
  42. package/adapters/claude-code/mods/ork-ponte/hooks/register.ts +331 -0
  43. package/adapters/claude-code/mods/ork-ponte/hooks/vigia.ts +76 -0
  44. package/adapters/claude-code/mods/ork-ponte/orkastery-mod.json +5 -0
  45. package/adapters/claude-code/mods/ork-ponte/types/index.d.ts +178 -0
  46. package/adapters/claude-code/mods/ork-portao/.claude-plugin/plugin.json +14 -0
  47. package/adapters/claude-code/mods/ork-portao/hooks/hooks.json +3 -0
  48. package/adapters/claude-code/mods/ork-portao/hooks/lease.ts +201 -0
  49. package/adapters/claude-code/mods/ork-portao/hooks/plano.ts +304 -0
  50. package/adapters/claude-code/mods/ork-portao/hooks/register.ts +339 -0
  51. package/adapters/claude-code/mods/ork-portao/hooks/verify.ts +152 -0
  52. package/adapters/claude-code/mods/ork-portao/orkastery-mod.json +5 -0
  53. package/adapters/claude-code/mods/ork-portao/types/index.d.ts +60 -0
  54. package/adapters/claude-code/mods/ork-radar/.claude-plugin/plugin.json +14 -0
  55. package/adapters/claude-code/mods/ork-radar/hooks/colisao.ts +420 -0
  56. package/adapters/claude-code/mods/ork-radar/hooks/hooks.json +3 -0
  57. package/adapters/claude-code/mods/ork-radar/hooks/mapa.tsx +322 -0
  58. package/adapters/claude-code/mods/ork-radar/hooks/register.tsx +470 -0
  59. package/adapters/claude-code/mods/ork-radar/orkastery-mod.json +5 -0
  60. package/adapters/claude-code/mods/ork-radar/types/index.d.ts +72 -0
  61. package/adapters/claude-code/mods/ork-sentinela/.claude-plugin/plugin.json +14 -0
  62. package/adapters/claude-code/mods/ork-sentinela/hooks/alertas.ts +85 -0
  63. package/adapters/claude-code/mods/ork-sentinela/hooks/despacho.ts +156 -0
  64. package/adapters/claude-code/mods/ork-sentinela/hooks/hooks.json +3 -0
  65. package/adapters/claude-code/mods/ork-sentinela/hooks/register.ts +511 -0
  66. package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +186 -0
  67. package/adapters/claude-code/mods/ork-sentinela/orkastery-mod.json +5 -0
  68. package/adapters/claude-code/mods/ork-sentinela/types/index.d.ts +65 -0
  69. package/adapters/claude-code/mods/ork-torre/.claude-plugin/plugin.json +14 -0
  70. package/adapters/claude-code/mods/ork-torre/hooks/agenda.ts +60 -0
  71. package/adapters/claude-code/mods/ork-torre/hooks/detalhe.tsx +236 -0
  72. package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +42 -0
  73. package/adapters/claude-code/mods/ork-torre/hooks/hooks.json +3 -0
  74. package/adapters/claude-code/mods/ork-torre/hooks/painel.tsx +344 -0
  75. package/adapters/claude-code/mods/ork-torre/hooks/rede.ts +97 -0
  76. package/adapters/claude-code/mods/ork-torre/hooks/register.tsx +437 -0
  77. package/adapters/claude-code/mods/ork-torre/orkastery-mod.json +5 -0
  78. package/adapters/claude-code/mods/ork-torre/types/index.d.ts +108 -0
  79. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +1 -1
  80. package/adapters/openclaw/README.md +2 -1
  81. package/adapters/openclaw/openclaw.plugin.json +1 -1
  82. package/adapters/openclaw/package.json +1 -1
  83. package/assets/docs-en/markdownlint-cli2.jsonc +22 -0
  84. package/assets/docs-en/padroes/documentacao-de-produto.md +135 -0
  85. package/assets/docs-en/padroes/roadmap-de-produto.md +100 -0
  86. package/assets/docs-en/produto/README.md +14 -0
  87. package/assets/docs-en/produto/_modelo-feature.md +62 -0
  88. package/assets/docs-en/roadmap/README.md +14 -0
  89. package/assets/docs-en/roadmap/_modelo-item.md +75 -0
  90. package/dist/adapters/claude-bg.js +77 -13
  91. package/dist/adapters/codex.js +10 -1
  92. package/dist/agents-md.js +24 -15
  93. package/dist/auditrun.js +17 -2
  94. package/dist/board.js +4 -3
  95. package/dist/branch-de-estado.js +23 -1
  96. package/dist/catalogo.js +33 -0
  97. package/dist/ci.js +13 -7
  98. package/dist/claims-conferir.js +342 -0
  99. package/dist/claims.js +3 -1
  100. package/dist/company-brain-client.js +36 -1
  101. package/dist/company-brain-journal.js +2 -1
  102. package/dist/company-brain-migration.js +36 -1
  103. package/dist/conducao-sentinela.js +388 -0
  104. package/dist/conducao-texto.js +12 -18
  105. package/dist/conducao.js +122 -30
  106. package/dist/conhecimento.js +490 -0
  107. package/dist/docs.js +22 -8
  108. package/dist/doctor-pulse-cron.js +182 -27
  109. package/dist/doctor-rede.js +18 -17
  110. package/dist/doctor.js +115 -111
  111. package/dist/entrega-pr.js +22 -21
  112. package/dist/escopo-escrita.js +10 -1
  113. package/dist/estado-thread.js +23 -1
  114. package/dist/experiencia-instalacao.js +45 -25
  115. package/dist/fabrica-batida.js +310 -0
  116. package/dist/fabrica-estado.js +38 -8
  117. package/dist/fabrica-publicar.js +3 -1
  118. package/dist/fila-pesada.js +444 -0
  119. package/dist/fix.js +3 -2
  120. package/dist/forja.js +1 -1
  121. package/dist/gates.js +3 -28
  122. package/dist/handoff.js +10 -6
  123. package/dist/hitl-local-receipt.js +2 -0
  124. package/dist/hitl-resumo.js +29 -3
  125. package/dist/hitl-tempo-parado.js +4 -4
  126. package/dist/hitl.js +29 -33
  127. package/dist/horario.js +14 -7
  128. package/dist/hosts.js +52 -1
  129. package/dist/impedimento.js +108 -14
  130. package/dist/index.js +623 -521
  131. package/dist/indice.js +7 -1
  132. package/dist/init.js +43 -47
  133. package/dist/instabilidade.js +144 -0
  134. package/dist/integracoes-locais.js +2 -0
  135. package/dist/intelligence-graph-cli.js +92 -21
  136. package/dist/intelligence-graph-contexto.js +56 -17
  137. package/dist/intelligence-graph-contract.js +2 -1
  138. package/dist/intelligence-graph-extract-md.js +56 -4
  139. package/dist/intelligence-graph-extract-ts.js +71 -3
  140. package/dist/intelligence-graph-query.js +66 -19
  141. package/dist/ledger.js +15 -1
  142. package/dist/liveness.js +6 -3
  143. package/dist/locale-ajuda.js +761 -0
  144. package/dist/locale-gerados.js +160 -0
  145. package/dist/locale-master.js +99 -0
  146. package/dist/locale-onboarding.js +92 -0
  147. package/dist/locale-phase.js +151 -0
  148. package/dist/locale-pulse.js +211 -0
  149. package/dist/locale-retry.js +385 -0
  150. package/dist/locale-roadmap.js +102 -0
  151. package/dist/locale-ship.js +494 -0
  152. package/dist/locale-thread.js +277 -0
  153. package/dist/locale-verify.js +125 -0
  154. package/dist/locale.js +714 -0
  155. package/dist/manifest.js +129 -3
  156. package/dist/maquina.js +23 -4
  157. package/dist/master-digest.js +6 -1
  158. package/dist/master-nota.js +4 -6
  159. package/dist/master.js +21 -24
  160. package/dist/mcp-artifacts.js +11 -2
  161. package/dist/mcp-git.js +23 -6
  162. package/dist/mcp-grafo.js +7 -4
  163. package/dist/mcp-server.js +8 -2
  164. package/dist/mcp-ship.js +3 -1
  165. package/dist/mcp-verify.js +6 -1
  166. package/dist/modos.js +3 -6
  167. package/dist/network-roadmap.js +5 -2
  168. package/dist/noctum-arco.js +911 -0
  169. package/dist/noctum-capacidade.js +141 -0
  170. package/dist/noctum-envelope.js +810 -0
  171. package/dist/noctum-execucao.js +304 -0
  172. package/dist/noctum-fila.js +280 -0
  173. package/dist/ocupacao.js +29 -2
  174. package/dist/onboarding.js +40 -21
  175. package/dist/orkmind.js +9 -2
  176. package/dist/orquestracao.js +13 -12
  177. package/dist/parado-no-condutor.js +34 -9
  178. package/dist/phase.js +94 -39
  179. package/dist/policies.js +4 -4
  180. package/dist/portfolio.js +4 -2
  181. package/dist/postura-local.js +234 -0
  182. package/dist/preflight.js +11 -10
  183. package/dist/procedencia.js +225 -0
  184. package/dist/projeto-alvo.js +127 -4
  185. package/dist/prompts.js +6 -2
  186. package/dist/pulse-delivery.js +13 -2
  187. package/dist/pulse.js +52 -19
  188. package/dist/ratelimit.js +4 -2
  189. package/dist/redacao-saida.js +63 -0
  190. package/dist/rede-status.js +3 -2
  191. package/dist/retry.js +232 -148
  192. package/dist/roadmap-reservas.js +20 -3
  193. package/dist/roadmap-status.js +129 -33
  194. package/dist/runtime-context.js +181 -3
  195. package/dist/runtimes.js +2 -1
  196. package/dist/session-events.js +7 -3
  197. package/dist/session-watcher-claude.js +7 -1
  198. package/dist/session-watcher.js +185 -33
  199. package/dist/sessoes-contas.js +1 -1
  200. package/dist/ship-publicar.js +800 -0
  201. package/dist/ship.js +84 -81
  202. package/dist/steal.js +77 -0
  203. package/dist/thread-close.js +3 -2
  204. package/dist/thread.js +153 -82
  205. package/dist/util.js +8 -1
  206. package/dist/verify.js +141 -52
  207. package/dist/vida-da-thread.js +254 -0
  208. package/dist/worktree-local.js +165 -0
  209. package/dist/worktree.js +20 -0
  210. package/dist/yaml.js +27 -8
  211. package/package.json +1 -1
  212. package/schemas/code-artifact-graph.v1.schema.json +2 -1
@@ -0,0 +1,270 @@
1
+ // Os arquivos tocados num lote e as claims que eles afetam (RM-059, F2, T2.2). Modulo puro: le a saida do
2
+ // `git status`, compara fotos, junta os caminhos das ferramentas de escrita do lote e casa os caminhos com as
3
+ // claims e com os `touch_paths` do plano. As chamadas ao `$` ficam no register.ts.
4
+ import type { FreioClaim, FreioFoto, FreioPedido, FreioVigia } from '../types'
5
+
6
+ /** O `git status` da foto (D4): HEAD, sem renomeacao (vira saida e entrada) e com cada arquivo novo listado. */
7
+ export const ARGV_DO_STATUS: readonly string[] = ['git', 'status', '--porcelain=v2', '--branch', '-z', '--no-renames', '--untracked-files=all']
8
+ export const TETO_DO_STATUS_MS = 10000
9
+ /** Caminhos sujos com `stat` por foto; acima disso, so o codigo XY entra na assinatura. */
10
+ export const TETO_DE_STAT = 300
11
+ /** Nome-base menor que isto nao casa com comando (D5). */
12
+ export const MINIMO_DO_NOME_BASE = 3
13
+
14
+ /** As ferramentas que escrevem um arquivo e o campo do caminho no `tool_input`. */
15
+ const ESCRITA: Readonly<Record<string, string>> = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' }
16
+ /** Ferramentas que so leem: o lote feito so delas nao roda o `git status` (D4). */
17
+ export const FERRAMENTAS_DE_LEITURA: readonly string[] = [
18
+ 'Read', 'Grep', 'Glob', 'LS', 'NotebookRead', 'WebFetch', 'WebSearch', 'ToolSearch',
19
+ 'TaskCreate', 'TaskGet', 'TaskList', 'TaskUpdate', 'TaskOutput',
20
+ 'mcp__orkastery__ork_thread_status', 'mcp__orkastery__ork_artifact_read', 'mcp__orkastery__ork_claims_list',
21
+ 'mcp__orkastery__ork_git_status', 'mcp__orkastery__ork_phase_list', 'mcp__orkastery__ork_hitl_pending',
22
+ 'mcp__orkastery__ork_observe',
23
+ ]
24
+ const EXTENSOES_DE_CODIGO = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'mts', 'cts'])
25
+ /** O CLI de claims dentro de um comando do `Bash` (pelo `ork` ou pelo `index.js` do CLI): o lote mexeu nas claims. */
26
+ const CLI_DE_CLAIMS = /(?:\bork|\bindex\.js)\s+claims\s+(?:add|retirar|verificar)\b/
27
+ /** Caminhos fora dos `touch_paths` citados por extenso num aviso; o resto vai pela contagem (D8). */
28
+ export const TETO_DE_CAMINHOS_NO_AVISO = 20
29
+
30
+ /** Uma chamada do lote como o `classic.PostToolBatch` a entrega. */
31
+ export type Chamada = { tool_name?: unknown; tool_input?: unknown }
32
+ export type EntradaDoStatus = { caminho: string; xy: string }
33
+
34
+ /** Pasta e caminho relativo com uma barra so. */
35
+ export function juntar(pasta: string, rel: string): string {
36
+ return pasta.replace(/\/+$/, '') + '/' + rel.replace(/^\/+/, '')
37
+ }
38
+
39
+ /**
40
+ * A saida de `git status --porcelain=v2 --branch -z`: o HEAD (`# branch.oid`, `null` antes do primeiro commit)
41
+ * e cada caminho sujo com o codigo XY. Entrada renomeada (`2`) pula o caminho de origem; nova vem como `??`.
42
+ */
43
+ export function lerStatus(saida: string): { head: string | null; entradas: EntradaDoStatus[] } {
44
+ let head: string | null = null
45
+ const entradas: EntradaDoStatus[] = []
46
+ const registros = saida.split('\0')
47
+ for (let i = 0; i < registros.length; i++) {
48
+ const r = registros[i] as string
49
+ if (r.startsWith('# branch.oid ')) {
50
+ const oid = r.slice('# branch.oid '.length).trim()
51
+ head = /^[0-9a-f]{7,64}$/.test(oid) ? oid : null
52
+ continue
53
+ }
54
+ const campos = r.split(' ')
55
+ const xy = campos[1] ?? ''
56
+ if (r.startsWith('1 ') && campos.length >= 9) entradas.push({ caminho: campos.slice(8).join(' '), xy })
57
+ else if (r.startsWith('2 ') && campos.length >= 10) { entradas.push({ caminho: campos.slice(9).join(' '), xy }); i++ }
58
+ else if (r.startsWith('u ') && campos.length >= 11) entradas.push({ caminho: campos.slice(10).join(' '), xy })
59
+ else if (r.startsWith('? ') && r.length > 2) entradas.push({ caminho: r.slice(2), xy: '??' })
60
+ }
61
+ return { head, entradas }
62
+ }
63
+
64
+ /** A assinatura de um caminho sujo: o XY e, com `stat`, o tamanho e o `mtimeMs` (D4). */
65
+ export function assinatura(xy: string, stat: { size?: unknown; mtimeMs?: unknown } | null | undefined, comStat: boolean): string {
66
+ if (!comStat) return xy
67
+ return stat && typeof stat.size === 'number' && typeof stat.mtimeMs === 'number' ? `${xy}|${stat.size}|${stat.mtimeMs}` : `${xy}|ausente`
68
+ }
69
+
70
+ /**
71
+ * Duas assinaturas do mesmo caminho dizem mudanca. A que ficou so com o XY (acima do teto de `stat`) nao se
72
+ * compara pelo `stat` da outra: o caminho que passa a borda do teto sem mudar nao vira tocado.
73
+ */
74
+ export function mudou(antes: string | undefined, agora: string): boolean {
75
+ if (antes === undefined) return true
76
+ if (antes === agora) return false
77
+ const [xyAntes, ...statAntes] = antes.split('|')
78
+ const [xyAgora, ...statAgora] = agora.split('|')
79
+ return xyAntes !== xyAgora || (statAntes.length > 0 && statAgora.length > 0)
80
+ }
81
+
82
+ /**
83
+ * Os caminhos que mudaram entre duas fotos (D4): novo na lista, com assinatura diferente, ou que saiu da lista
84
+ * com o HEAD parado (reversao). Saida da lista com HEAD novo foi commit e nao conta. Sem foto anterior, nada.
85
+ */
86
+ export function diferenca(antes: FreioFoto | null, agora: FreioFoto): string[] {
87
+ if (!antes) return []
88
+ const tocados = new Set<string>()
89
+ for (const [caminho, sig] of Object.entries(agora.caminhos)) if (mudou(antes.caminhos[caminho], sig)) tocados.add(caminho)
90
+ if (antes.head === agora.head) for (const caminho of Object.keys(antes.caminhos)) if (!(caminho in agora.caminhos)) tocados.add(caminho)
91
+ return [...tocados].sort()
92
+ }
93
+
94
+ /** O caminho relativo a raiz da sessao, ou `null` quando ele fica fora dela ou nao e caminho. */
95
+ export function relativo(caminho: unknown, raiz: string): string | null {
96
+ if (typeof caminho !== 'string' || caminho.trim() === '') return null
97
+ const base = raiz.replace(/\/+$/, '')
98
+ let rel = caminho.trim()
99
+ if (rel.startsWith('/')) {
100
+ if (!rel.startsWith(base + '/')) return null
101
+ rel = rel.slice(base.length + 1)
102
+ }
103
+ rel = rel.replace(/^(?:\.\/)+/, '')
104
+ const partes = rel.split('/')
105
+ return rel !== '' && !partes.some(p => p === '' || p === '.' || p === '..') ? rel : null
106
+ }
107
+
108
+ const nomeDa = (c: Chamada) => (typeof c?.tool_name === 'string' ? c.tool_name : '')
109
+
110
+ /** Os caminhos que as ferramentas de escrita do lote citam, dentro da raiz, sem repeticao. */
111
+ export function caminhosDoLote(chamadas: readonly Chamada[], raiz: string): string[] {
112
+ const achados = new Set<string>()
113
+ for (const c of chamadas) {
114
+ const campo = ESCRITA[nomeDa(c)]
115
+ if (!campo) continue
116
+ const rel = relativo((c.tool_input as Record<string, unknown> | null)?.[campo], raiz)
117
+ if (rel) achados.add(rel)
118
+ }
119
+ return [...achados].sort()
120
+ }
121
+
122
+ /** O lote so tem ferramentas de leitura (ou nenhuma chamada): nao ha o que fotografar. */
123
+ export function soLeitura(chamadas: readonly Chamada[]): boolean {
124
+ return chamadas.every(c => FERRAMENTAS_DE_LEITURA.includes(nomeDa(c)))
125
+ }
126
+
127
+ /** O lote registrou, retirou ou mudou claim: a lista do nucleo precisa ser relida. */
128
+ export function mexeuNasClaims(chamadas: readonly Chamada[]): boolean {
129
+ return chamadas.some(c => nomeDa(c) === 'mcp__orkastery__ork_claim_add'
130
+ || (nomeDa(c) === 'Bash' && CLI_DE_CLAIMS.test(String((c.tool_input as { command?: unknown } | null)?.command ?? ''))))
131
+ }
132
+
133
+ /** O lote regravou um artefato de fase: o plano precisa ser relido. */
134
+ export function mexeuNoPlano(chamadas: readonly Chamada[]): boolean {
135
+ return chamadas.some(c => nomeDa(c) === 'mcp__orkastery__ork_artifact_write')
136
+ }
137
+
138
+ const fronteira = (c: string) => c === '' || /[\s'"=:(),;|&<>]/.test(c)
139
+
140
+ /** O texto cita `alvo` com fronteira dos dois lados. */
141
+ function cita(texto: string, alvo: string, antes: (c: string) => boolean): boolean {
142
+ for (let i = texto.indexOf(alvo); i >= 0; i = texto.indexOf(alvo, i + 1)) {
143
+ if (antes(i === 0 ? '' : (texto[i - 1] as string)) && fronteira(texto[i + alvo.length] ?? '')) return true
144
+ }
145
+ return false
146
+ }
147
+
148
+ /** O comando cita o caminho inteiro, com ou sem `./` na frente (D5, forca 2). */
149
+ export function citaCaminho(comando: string, caminho: string): boolean {
150
+ return cita(comando, caminho, fronteira) || cita(comando, './' + caminho, fronteira)
151
+ }
152
+
153
+ /**
154
+ * O comando cita o mesmo nome-base de um fonte TS ou JS, com extensao de codigo (D5, forca 1): pega o teste
155
+ * compilado (`core/test/x.test.ts` contra `core/dist-test/test/x.test.js`). Nome-base curto nao casa.
156
+ */
157
+ export function citaNomeBase(comando: string, caminho: string): boolean {
158
+ const base = caminho.slice(caminho.lastIndexOf('/') + 1)
159
+ const ponto = base.lastIndexOf('.')
160
+ if (ponto <= 0 || !EXTENSOES_DE_CODIGO.has(base.slice(ponto + 1))) return false
161
+ const nome = base.slice(0, ponto)
162
+ if (nome.length < MINIMO_DO_NOME_BASE) return false
163
+ const alvo = nome + '.'
164
+ for (let i = comando.indexOf(alvo); i >= 0; i = comando.indexOf(alvo, i + 1)) {
165
+ const antes = i === 0 ? '' : (comando[i - 1] as string)
166
+ if (antes !== '/' && !fronteira(antes)) continue
167
+ let j = i + alvo.length
168
+ while (j < comando.length && /[a-z]/.test(comando[j] as string)) j++
169
+ if (EXTENSOES_DE_CODIGO.has(comando.slice(i + alvo.length, j)) && fronteira(comando[j] ?? '')) return true
170
+ }
171
+ return false
172
+ }
173
+
174
+ const numero = (id: string) => Number(id.slice(1)) || 0
175
+
176
+ /** Ordem da fila: a forca maior antes; na mesma forca, a claim mais nova antes. */
177
+ function ordenar(pedidos: FreioPedido[]): FreioPedido[] {
178
+ return pedidos.sort((a, b) => b.forca - a.forca || numero(b.id) - numero(a.id))
179
+ }
180
+
181
+ /**
182
+ * As claims que os caminhos tocados afetam, com a forca e os caminhos de cada uma (D5). Claim sem comando fica de
183
+ * fora: a conferencia so diria que ela nao tem como ser verificada.
184
+ */
185
+ export function claimsAfetadas(claims: readonly FreioClaim[], tocados: readonly string[]): FreioPedido[] {
186
+ const afetadas: FreioPedido[] = []
187
+ for (const c of claims) {
188
+ if (c.comandos.length === 0) continue
189
+ let forca = 0
190
+ const caminhos: string[] = []
191
+ for (const t of tocados) {
192
+ const f = c.arquivo === t ? 3 : c.comandos.some(cmd => citaCaminho(cmd, t)) ? 2 : c.comandos.some(cmd => citaNomeBase(cmd, t)) ? 1 : 0
193
+ if (f > 0) { caminhos.push(t); forca = Math.max(forca, f) }
194
+ }
195
+ if (forca > 0) afetadas.push({ id: c.id, forca, caminhos })
196
+ }
197
+ return ordenar(afetadas)
198
+ }
199
+
200
+ /** A fila com as claims novas, sem repeticao: a claim que ja esta nela junta os caminhos e fica com a forca maior. */
201
+ export function enfileirar(fila: readonly FreioPedido[], afetadas: readonly FreioPedido[]): FreioPedido[] {
202
+ const porId = new Map(fila.map(p => [p.id, { ...p, caminhos: [...p.caminhos] }]))
203
+ for (const a of afetadas) {
204
+ const ja = porId.get(a.id)
205
+ if (!ja) { porId.set(a.id, { ...a, caminhos: [...a.caminhos] }); continue }
206
+ ja.forca = Math.max(ja.forca, a.forca)
207
+ for (const c of a.caminhos) if (!ja.caminhos.includes(c)) ja.caminhos.push(c)
208
+ }
209
+ return ordenar([...porId.values()])
210
+ }
211
+
212
+ /** O caminho casa com um `touch_path`: caminho exato ou pasta (com ou sem barra no fim), `*` sem barra e `**` (D9). */
213
+ export function casaTouchPath(caminho: string, padrao: string): boolean {
214
+ const p = padrao.replace(/\/+$/, '')
215
+ if (p === '') return false
216
+ if (!p.includes('*')) return caminho === p || caminho.startsWith(p + '/')
217
+ let fonte = '^'
218
+ for (let i = 0; i < p.length; i++) {
219
+ const c = p[i] as string
220
+ if (c === '*') {
221
+ if (p[i + 1] === '*') { fonte += '.*'; i++ } else fonte += '[^/]*'
222
+ } else fonte += /[.+?^${}()|[\]\\]/.test(c) ? '\\' + c : c
223
+ }
224
+ return new RegExp(fonte + '$').test(caminho)
225
+ }
226
+
227
+ /** Os caminhos tocados fora da uniao dos `touch_paths` do plano; plano sem `touch_paths` nao avisa nada (D9). */
228
+ export function foraDosTouchPaths(tocados: readonly string[], touchPaths: readonly string[] | null): string[] {
229
+ if (!touchPaths || touchPaths.length === 0) return []
230
+ return tocados.filter(t => !touchPaths.some(p => casaTouchPath(t, p)))
231
+ }
232
+
233
+ /** A vigia de uma thread antes da primeira foto. */
234
+ export function vigiaVazia(thread: string): FreioVigia {
235
+ return { thread, foto: null, seq: 0, toques: {}, fila: [], avisadosFora: [], foraPendentes: [] }
236
+ }
237
+
238
+ /**
239
+ * O que um lote faz na vigia: foto nova, toques, claims afetadas na fila e os caminhos novos fora dos `touch_paths`,
240
+ * guardados uma vez cada para o aviso do laco principal (o lote de subagente toca, mas nao recebe o aviso).
241
+ */
242
+ export function aplicarLote(v: FreioVigia, lote: { foto: FreioFoto | null; doLote: readonly string[]; claims: readonly FreioClaim[] | null; touchPaths: readonly string[] | null }):
243
+ { vigia: FreioVigia; tocados: string[]; afetadas: FreioPedido[]; fora: string[] } {
244
+ const tocados = [...new Set([...lote.doLote, ...(lote.foto ? diferenca(v.foto, lote.foto) : [])])].sort()
245
+ const seq = v.seq + 1
246
+ const toques = { ...v.toques }
247
+ for (const t of tocados) toques[t] = seq
248
+ const afetadas = lote.claims ? claimsAfetadas(lote.claims, tocados) : []
249
+ const fora = foraDosTouchPaths(tocados, lote.touchPaths).filter(c => !v.avisadosFora.includes(c))
250
+ return {
251
+ vigia: {
252
+ ...v, foto: lote.foto ?? v.foto, seq, toques, fila: enfileirar(v.fila, afetadas),
253
+ avisadosFora: [...v.avisadosFora, ...fora], foraPendentes: [...(v.foraPendentes ?? []), ...fora],
254
+ },
255
+ tocados, afetadas, fora,
256
+ }
257
+ }
258
+
259
+ /** Tira os caminhos fora dos `touch_paths` ainda nao avisados, para o lote do laco principal devolver. */
260
+ export function tomarFora(v: FreioVigia): { vigia: FreioVigia; fora: string[] } {
261
+ return { vigia: { ...v, foraPendentes: [] }, fora: v.foraPendentes ?? [] }
262
+ }
263
+
264
+ /** O aviso de `touch_paths` para o modelo, com teto de caminhos; sem caminho novo fora do plano, nenhum texto. */
265
+ export function textoDoFora(fora: readonly string[]): string {
266
+ if (fora.length === 0) return ''
267
+ const citados = fora.slice(0, TETO_DE_CAMINHOS_NO_AVISO).join(', ')
268
+ const resto = fora.length > TETO_DE_CAMINHOS_NO_AVISO ? ` e mais ${fora.length - TETO_DE_CAMINHOS_NO_AVISO}` : ''
269
+ return `Freio de Lote: o lote mexeu fora dos touch_paths do plano: ${citados}${resto}.`
270
+ }
@@ -0,0 +1,285 @@
1
+ // O Freio de Lote (RM-059, fatia F2). Na sessao de fase, acha as claims que o lote de ferramentas mexeu, pede
2
+ // ao nucleo a conferencia delas no sandbox do verify (`ork claims conferir`) e devolve o que quebrou no lote
3
+ // seguinte. Nunca para o laco agentico, nunca roda comando de claim, nunca grava estado de claim e nunca fala
4
+ // pelo dono. A logica fica nos modulos puros ao lado; aqui so as chamadas ao `$`, sempre escritas como
5
+ // `x.noun.metodo(...)`, como o validador exige.
6
+ import type { EngineInterface, Register } from 'claude-code'
7
+ import type { FreioCarga, FreioFoto, FreioVigia } from '../types'
8
+ import {
9
+ argsDaFerramenta, cargaVazia, claimsDoNucleo, claimsVelhas, FERRAMENTA_DAS_CLAIMS, FERRAMENTA_DO_PLANO, jsonDoMcp,
10
+ maiorNumeroDeClaim, planoVelho, SERVIDOR, statusNoRetrato, textoDoPlano, threadAtiva, touchPathsDoPlano,
11
+ } from './carga'
12
+ import {
13
+ aplicarLote, ARGV_DO_STATUS, assinatura, caminhosDoLote, diferenca, juntar, lerStatus, mexeuNasClaims, mexeuNoPlano,
14
+ soLeitura, TETO_DE_STAT, TETO_DO_STATUS_MS, textoDoFora, tomarFora, vigiaVazia,
15
+ } from './lote'
16
+ import type { Chamada } from './lote'
17
+ import {
18
+ acumular, argvDaConferencia, ESPERA_DO_FIM_MS, fecharRodada, indisponivelSemRodada, iniciarRodada, lerConferencia, limitarTexto,
19
+ MOTIVO_PROCESSO_ABERTO, precisaDeLaco, TETO_POR_RODADA, textoDaEntrega, textoDoStatus, tomarEntrega, VIGIA_DA_CONFERENCIA_MS,
20
+ } from './conferencia'
21
+ import type { Leitura } from './conferencia'
22
+
23
+ const CARGA = { plugin: 'ork-freio', key: 'carga' } as const
24
+ const VIGIA = { plugin: 'ork-freio', key: 'vigia' } as const
25
+ const TENTATIVAS_DE_ESCRITA = 5
26
+ /**
27
+ * O laco de conferencia desta carga do modulo esta rodando (D10). Fica no modulo, nao no `$.state`: uma troca do
28
+ * worker mata o laco junto com a marca, e a rodada que ficou aberta no estado e retomada pelo laco seguinte.
29
+ */
30
+ let rodando = false
31
+ /**
32
+ * O processo da ultima rodada ainda nao fechou (o vigia venceu e o `return` nao chegou a ele, como num processo que
33
+ * nao escreve nada). Enquanto ele existir, nenhuma rodada nova abre: duas conferencias nunca correm juntas.
34
+ */
35
+ let fluxoAberto: Promise<unknown> | null = null
36
+
37
+ /** A sessao de fase com thread, pelo contexto da ponte; qualquer outra sessao fica sem Freio (D10). */
38
+ type ContextoDeFase = { thread: string; raiz: string }
39
+
40
+ const mensagem = (erro: unknown) => (erro instanceof Error ? erro.message : String(erro)).slice(0, 300)
41
+
42
+ async function contextoDeFase($: EngineInterface): Promise<ContextoDeFase | null> {
43
+ const ctx = await $.ork.contexto()
44
+ return ctx.papel === 'fase' && ctx.thread ? { thread: ctx.thread, raiz: ctx.raiz } : null
45
+ }
46
+
47
+ /** Grava a carga com `ifVersion`, aplicando `mudar` sobre a versao mais nova (como a ponte faz com a faixa). */
48
+ async function atualizarCarga($: EngineInterface, thread: string, mudar: (atual: FreioCarga) => FreioCarga): Promise<FreioCarga> {
49
+ for (let i = 0; i < TENTATIVAS_DE_ESCRITA; i++) {
50
+ const lida = await $.state.get(CARGA)
51
+ const atual = lida.value && lida.value.thread === thread ? lida.value : cargaVazia(thread)
52
+ const nova = mudar(atual)
53
+ const escrita = await $.state.set(CARGA, nova, { ifVersion: lida.version })
54
+ if (escrita.isSet) return nova
55
+ }
56
+ throw new Error('ork-freio: a carga nao foi guardada')
57
+ }
58
+
59
+ /** Grava a vigia do mesmo jeito; `mudar` e pura e pode rodar de novo quando outra escrita passou na frente. */
60
+ async function atualizarVigia<T>($: EngineInterface, thread: string, mudar: (atual: FreioVigia) => { vigia: FreioVigia; extra: T }): Promise<{ vigia: FreioVigia; extra: T }> {
61
+ for (let i = 0; i < TENTATIVAS_DE_ESCRITA; i++) {
62
+ const lida = await $.state.get(VIGIA)
63
+ const atual = lida.value && lida.value.thread === thread ? lida.value : vigiaVazia(thread)
64
+ const r = mudar(atual)
65
+ const escrita = await $.state.set(VIGIA, r.vigia, { ifVersion: lida.version })
66
+ if (escrita.isSet) return r
67
+ }
68
+ throw new Error('ork-freio: a vigia nao foi guardada')
69
+ }
70
+
71
+ /**
72
+ * A carga (T2.1): o estado da thread pelo retrato da ponte, as claims e o plano pelas duas ferramentas de
73
+ * leitura do nucleo (D2). Cada parte falha sozinha e fica para a proxima leitura.
74
+ */
75
+ async function carregar($: EngineInterface, ctx: ContextoDeFase, partes: { retrato: boolean; claims: boolean; plano: boolean }): Promise<FreioCarga> {
76
+ const agora = await $.clock.now()
77
+ const erros: string[] = []
78
+ let status: string | null | undefined
79
+ let claims: FreioCarga['claims'] | undefined
80
+ let maiorClaim = 0
81
+ let touchPaths: string[] | null = null
82
+ let planoLido = false
83
+ if (partes.retrato) {
84
+ try {
85
+ status = statusNoRetrato((await $.ork.retrato({ thread: ctx.thread })).retrato, ctx.thread)
86
+ } catch (erro) { erros.push(`retrato: ${mensagem(erro)}`) }
87
+ }
88
+ if (partes.claims) {
89
+ try {
90
+ const r = await $.mcp.call(SERVIDOR, FERRAMENTA_DAS_CLAIMS, argsDaFerramenta(FERRAMENTA_DAS_CLAIMS, ctx.thread))
91
+ const lista = jsonDoMcp(r, FERRAMENTA_DAS_CLAIMS)
92
+ claims = claimsDoNucleo(lista)
93
+ maiorClaim = maiorNumeroDeClaim(lista)
94
+ } catch (erro) { erros.push(`claims: ${mensagem(erro)}`) }
95
+ }
96
+ if (partes.plano) {
97
+ try {
98
+ const r = await $.mcp.call(SERVIDOR, FERRAMENTA_DO_PLANO, argsDaFerramenta(FERRAMENTA_DO_PLANO, ctx.thread))
99
+ touchPaths = touchPathsDoPlano(textoDoPlano(jsonDoMcp(r, FERRAMENTA_DO_PLANO)))
100
+ planoLido = true
101
+ } catch (erro) { erros.push(`plano: ${mensagem(erro)}`) }
102
+ }
103
+ // Uma leitura mais velha nunca sobrescreve outra mais nova que gravou antes dela (duas cargas ao mesmo tempo).
104
+ return atualizarCarga($, ctx.thread, atual => ({
105
+ ...atual,
106
+ ...(status !== undefined ? { statusDaThread: status } : {}),
107
+ ...(claims !== undefined && agora >= atual.claimsEm ? { claims, claimsEm: agora, maiorClaim } : {}),
108
+ ...(planoLido && agora >= atual.planoEm ? { touchPaths, planoLido: true, planoEm: agora } : {}),
109
+ erros,
110
+ }))
111
+ }
112
+
113
+ /** A foto do `git status` da worktree (D4), com o `stat` de cada caminho sujo ate o teto; `null` se o git falhar. */
114
+ async function fotografar($: EngineInterface, raiz: string): Promise<FreioFoto | null> {
115
+ let r
116
+ try {
117
+ // Sem lock opcional: o `git status` do Freio nunca segura o `index.lock` que um commit do modelo precisa.
118
+ r = await $.process.run(ARGV_DO_STATUS, { cwd: raiz, timeoutMs: TETO_DO_STATUS_MS, env: { GIT_OPTIONAL_LOCKS: '0' } })
119
+ } catch { return null }
120
+ if (r.exitCode !== 0) return null
121
+ const lido = lerStatus(r.stdout)
122
+ const caminhos: Record<string, string> = {}
123
+ for (let i = 0; i < lido.entradas.length; i++) {
124
+ const entrada = lido.entradas[i] as { caminho: string; xy: string }
125
+ const comStat = i < TETO_DE_STAT
126
+ const stat = comStat ? await $.fs.stat(juntar(raiz, entrada.caminho)).catch(() => null) : null
127
+ caminhos[entrada.caminho] = assinatura(entrada.xy, stat, comStat)
128
+ }
129
+ return { head: lido.head, caminhos }
130
+ }
131
+
132
+ /** A primeira foto, guardada so quando ainda nao ha foto (o reload nao apaga o que o lote anterior viu). */
133
+ async function primeiraFoto($: EngineInterface, ctx: ContextoDeFase): Promise<void> {
134
+ const foto = await fotografar($, ctx.raiz)
135
+ if (!foto) return
136
+ await atualizarVigia($, ctx.thread, v => ({ vigia: v.foto ? v : { ...v, foto }, extra: null }))
137
+ }
138
+
139
+ /**
140
+ * Uma rodada no nucleo (D6): `ork claims conferir` por `$.process.spawn`, na raiz da sessao, com o vigia do mod.
141
+ * O processo que nao abre e a saida fora do formato viram conferencia indisponivel (D7).
142
+ */
143
+ async function rodarConferencia($: EngineInterface, ctx: ContextoDeFase, ids: readonly string[]): Promise<Leitura> {
144
+ let stdout = ''
145
+ let stderr = ''
146
+ let vigia: { cancel: () => void } | undefined
147
+ try {
148
+ const fluxo = $.process.spawn({ argv: argvDaConferencia(ctx.thread, ids), cwd: ctx.raiz })
149
+ const lido = (async () => {
150
+ for await (const pedaco of fluxo) {
151
+ if (pedaco.stream === 'stdout') stdout = acumular(stdout, pedaco.text, false)
152
+ else stderr = acumular(stderr, pedaco.text, true)
153
+ }
154
+ return fluxo.result
155
+ })()
156
+ lido.catch(() => undefined)
157
+ fluxoAberto = lido
158
+ void lido.catch(() => undefined).then(() => { if (fluxoAberto === lido) fluxoAberto = null })
159
+ const vencido = new Promise<'vencido'>(resolver => { vigia = $.clock.after(VIGIA_DA_CONFERENCIA_MS, () => resolver('vencido')) })
160
+ const fim = await Promise.race([lido, vencido])
161
+ if (fim === 'vencido') {
162
+ // Pede o fim do processo; a volta seguinte do laco espera ele fechar (uma espera so, com teto) antes de abrir
163
+ // rodada nova, e com ele aberto a conferencia fica indisponivel.
164
+ void fluxo.return(undefined as never).catch(() => undefined)
165
+ return { tipo: 'prazo' }
166
+ }
167
+ return lerConferencia(stdout, fim.code, stderr)
168
+ } catch (erro) {
169
+ return { tipo: 'indisponivel', motivo: `ork claims conferir nao rodou: ${mensagem(erro)}` }
170
+ } finally {
171
+ vigia?.cancel()
172
+ }
173
+ }
174
+
175
+ /** As claims ativas da carga e o maior numero ja dado, para a rodada nao levar claim retirada; `null` sem lista lida. */
176
+ async function claimsAtivas($: EngineInterface, ctx: ContextoDeFase): Promise<{ ids: string[]; maior: number } | null> {
177
+ const carga = (await $.state.get(CARGA)).value
178
+ return carga && carga.thread === ctx.thread && carga.claims ? { ids: carga.claims.map(c => c.id), maior: carga.maiorClaim ?? 0 } : null
179
+ }
180
+
181
+ /**
182
+ * O laco de fundo (T2.3): uma rodada por vez, ate a fila acabar, a pausa chegar ou a conferencia faltar. A recusa
183
+ * de uma claim so rele as claims e segue com as outras; a conducao alheia pausa um minuto.
184
+ */
185
+ async function conferirEmLaco($: EngineInterface, ctx: ContextoDeFase): Promise<void> {
186
+ if (rodando) return
187
+ rodando = true
188
+ try {
189
+ for (;;) {
190
+ if (fluxoAberto) {
191
+ const aberto = fluxoAberto
192
+ await Promise.race([aberto.catch(() => undefined), new Promise(resolver => { $.clock.after(ESPERA_DO_FIM_MS, () => resolver(undefined)) })])
193
+ if (fluxoAberto === aberto) {
194
+ const quando = await $.clock.now()
195
+ const parada = await atualizarVigia($, ctx.thread, v => ({ vigia: indisponivelSemRodada(v, MOTIVO_PROCESSO_ABERTO, quando), extra: null }))
196
+ $.ui.status(textoDoStatus(parada.vigia, quando))
197
+ break
198
+ }
199
+ }
200
+ const agora = await $.clock.now()
201
+ const ativas = await claimsAtivas($, ctx)
202
+ const { vigia } = await atualizarVigia($, ctx.thread, v => ({ vigia: iniciarRodada(v, agora, TETO_POR_RODADA, ativas), extra: null }))
203
+ if (!vigia.rodada) break
204
+ $.ui.status(textoDoStatus(vigia, agora))
205
+ const leitura = await rodarConferencia($, ctx, vigia.rodada.ids)
206
+ const depois = await $.clock.now()
207
+ const fechada = await atualizarVigia($, ctx.thread, v => ({ vigia: fecharRodada(v, leitura, depois), extra: null }))
208
+ $.ui.status(textoDoStatus(fechada.vigia, depois))
209
+ if (leitura.tipo === 'recusada') await carregar($, ctx, { retrato: false, claims: true, plano: false })
210
+ if (leitura.tipo === 'indisponivel' || leitura.tipo === 'ocupada') break
211
+ }
212
+ } finally {
213
+ rodando = false
214
+ }
215
+ }
216
+
217
+ /** Abre o laco fora do despacho do lote, por `$.clock.after`; o laco que ja roda pega a fila nova. */
218
+ function pedirConferencia($: EngineInterface, ctx: ContextoDeFase): void {
219
+ if (rodando) return
220
+ $.clock.after(0, () => { void conferirEmLaco($, ctx).catch(() => undefined) })
221
+ }
222
+
223
+ export const register: Register = on => {
224
+ // A carga roda fora do inicio da sessao: o MCP pode ainda estar conectando, e o inicio nao espera por ela.
225
+ // O `session.start` dispara de novo num reload, e a carga refaz a leitura do mesmo jeito (D10).
226
+ on('session.start', async ($, e, next) => {
227
+ const iniciada = await next(e)
228
+ try {
229
+ const ctx = await contextoDeFase($)
230
+ if (ctx) {
231
+ $.clock.after(0, () => {
232
+ void (async () => {
233
+ await carregar($, ctx, { retrato: true, claims: true, plano: true }).catch(() => undefined)
234
+ await primeiraFoto($, ctx).catch(() => undefined)
235
+ })()
236
+ })
237
+ }
238
+ } catch { /* sem a ponte, o Freio fica dormente */ }
239
+ return iniciada
240
+ })
241
+
242
+ // O lote (T2.2 e T2.3): devolve o que a conferencia apurou desde o lote anterior, acha os arquivos tocados e as
243
+ // claims afetadas, pede a conferencia ao nucleo e avisa os `touch_paths`. Responde so com `additionalContext` e
244
+ // nunca para o laco: na sessao de fundo nao ha quem o retome. Falha deixa o lote passar. O lote de subagente
245
+ // (`agent_id`) conta os toques, mas a entrega e o aviso ficam para o lote do laco principal, que e quem os le.
246
+ on('classic.PostToolBatch', async ($, e, next) => {
247
+ const resultado = await next(e)
248
+ const ctx = await contextoDeFase($)
249
+ if (!ctx) return resultado
250
+ let carga = (await $.state.get(CARGA)).value
251
+ if (carga && carga.thread === ctx.thread && !threadAtiva(carga.statusDaThread)) return resultado
252
+ const chamadas: Chamada[] = Array.isArray(e.tool_calls) ? e.tool_calls : []
253
+ const doSubagente = typeof e.agent_id === 'string' && e.agent_id !== ''
254
+ const doLote = caminhosDoLote(chamadas, ctx.raiz)
255
+ const foto = soLeitura(chamadas) ? null : await fotografar($, ctx.raiz)
256
+ const mexeu = doLote.length > 0 || foto !== null
257
+ // O lote que mexeu nas claims ou no plano rele na hora; a lista velha so e relida quando ha caminho para casar.
258
+ const lida = (await $.state.get(VIGIA)).value
259
+ const anterior = lida && lida.thread === ctx.thread ? lida.foto : null
260
+ const previstos = doLote.length + (foto ? diferenca(anterior, foto).length : 0)
261
+ const nasClaims = mexeuNasClaims(chamadas)
262
+ const noPlano = mexeuNoPlano(chamadas)
263
+ if (previstos > 0 || nasClaims || noPlano) {
264
+ const agora = await $.clock.now()
265
+ const atual = carga && carga.thread === ctx.thread ? carga : cargaVazia(ctx.thread)
266
+ const relerClaims = nasClaims || (previstos > 0 && claimsVelhas(atual, agora, false))
267
+ const relerPlano = noPlano || (previstos > 0 && planoVelho(atual, agora, false))
268
+ if (relerClaims || relerPlano) carga = await carregar($, ctx, { retrato: false, claims: relerClaims, plano: relerPlano })
269
+ }
270
+ const lote = { foto, doLote, claims: carga?.thread === ctx.thread ? carga.claims : null, touchPaths: carga?.thread === ctx.thread ? carga.touchPaths : null }
271
+ const { vigia, extra } = await atualizarVigia($, ctx.thread, v => {
272
+ const aplicada = mexeu ? aplicarLote(v, lote).vigia : v
273
+ if (doSubagente) return { vigia: aplicada, extra: { entrega: null, fora: [] as string[] } }
274
+ const f = tomarFora(aplicada)
275
+ const t = tomarEntrega(f.vigia)
276
+ return { vigia: t.vigia, extra: { entrega: t.entrega, fora: f.fora } }
277
+ })
278
+ // A rodada que ficou aberta no estado (o laco morreu com o worker) e retomada como a fila.
279
+ if (precisaDeLaco(vigia)) pedirConferencia($, ctx)
280
+ $.ui.status(textoDoStatus(vigia, await $.clock.now()))
281
+ const partes = [extra.entrega ? textoDaEntrega(extra.entrega) : '', textoDoFora(extra.fora)].filter(t => t !== '')
282
+ const texto = limitarTexto(partes.join('\n'))
283
+ return texto ? { ...resultado, additionalContext: [...(resultado.additionalContext ?? []), texto] } : resultado
284
+ }).catch(($, e, next) => next(e))
285
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "formato": "ork.mod/v1",
3
+ "papeis": ["fase"],
4
+ "claudeCodeMinimo": "2.1.287"
5
+ }