@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
@@ -24,6 +24,7 @@ const ledger_1 = require("./ledger");
24
24
  const modos_1 = require("./modos");
25
25
  const thread_1 = require("./thread");
26
26
  const util_1 = require("./util");
27
+ const locale_1 = require("./locale");
27
28
  /** O merge `ship(<thread>)` mais recente na base remota, com o head do PR (segundo pai). */
28
29
  function mergeDaEntrega(raiz, thread, remoto, base) {
29
30
  const r = (0, util_1.exec)('git', ['log', `refs/remotes/${remoto}/${base}`, '--first-parent', '--format=%H%x09%P%x09%s', `--grep=^ship(${thread})`], raiz);
@@ -64,32 +65,32 @@ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
64
65
  const base = carregado.manifesto.worktree.base_branch;
65
66
  const thread = (0, thread_1.lerThread)(raiz, threadId);
66
67
  if (thread.status === 'fechada')
67
- return resultado(threadId, 'ja-registrada', 'thread ja fechada');
68
+ return resultado(threadId, 'ja-registrada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prThreadFechada));
68
69
  if (opcoes.buscar !== false)
69
70
  (0, util_1.exec)('git', ['fetch', '-q', '--', remoto, `+refs/heads/${base}:refs/remotes/${remoto}/${base}`], raiz);
70
71
  const encontrado = mergeDaEntrega(raiz, threadId, remoto, base);
71
72
  if (!encontrado)
72
- return resultado(threadId, 'sem-merge', `nenhum merge ship(${threadId}) em ${remoto}/${base}`);
73
+ return resultado(threadId, 'sem-merge', (0, locale_1.textoDoCatalogo)((m) => m.ship.prSemMerge(threadId, remoto, base)));
73
74
  // RM-044 (A3): o condutor roda o registrar-pr logo depois do merge; e aqui que ele fica sabendo da pagina que
74
75
  // vai deixar o push da base vermelho, lida da base remota que o fetch acabou de trazer.
75
76
  const merge = { ...encontrado, docsPendentes: (0, docs_1.docsPendentesDoMerge)(raiz, threadId, `refs/remotes/${remoto}/${base}`) };
76
77
  const dir = (0, thread_1.dirThread)(raiz, threadId);
77
78
  const jaTem = (0, ledger_1.lerLedger)(dir).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.shipConcluido && e.mergeSha === merge.mergeSha);
78
79
  if (jaTem)
79
- return resultado(threadId, 'ja-registrada', 'esta entrega ja tem ship_done', merge);
80
+ return resultado(threadId, 'ja-registrada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prJaTemShipDone), merge);
80
81
  // A ponta do remoto AGORA, e o merge dentro dela: e o que torna o push provado, nao presumido.
81
82
  const remotoAgora = (0, util_1.exec)('git', ['ls-remote', '--', remoto, `refs/heads/${base}`], raiz);
82
83
  const ponta = remotoAgora.ok ? remotoAgora.stdout.split('\t')[0].trim() : '';
83
84
  if (!ponta || !(0, util_1.exec)('git', ['merge-base', '--is-ancestor', merge.mergeSha, ponta], raiz).ok) {
84
- return resultado(threadId, 'recusada', `o merge ${merge.mergeSha.slice(0, 7)} nao esta na ponta de ${remoto}/${base}`, merge);
85
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prForaDaPonta(merge.mergeSha.slice(0, 7), remoto, base)), merge);
85
86
  }
86
87
  const ci = (0, ci_1.consultarCi)(carregado, merge.headSha ?? merge.mergeSha, remoto, opcoes.executorCi);
87
88
  if (ci.required && !ci.ok) {
88
- return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { ...merge, ci });
89
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prSemCiVerde((0, locale_1.remontar)(ci.detail))), { ...merge, ci });
89
90
  }
90
91
  // RM-037 (fatia 3, defeito 6): o ensaio confere tudo e para aqui, sem ledger, fase nem fabrica.
91
92
  if (opcoes.dryRun) {
92
- return resultado(threadId, 'registraria', `ensaio: registraria ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR (nada gravado)`, { ...merge, ci });
93
+ return resultado(threadId, 'registraria', (0, locale_1.textoDoCatalogo)((m) => m.ship.prRegistraria(merge.mergeSha.slice(0, 7))), { ...merge, ci });
93
94
  }
94
95
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
95
96
  de: thread.base?.branch ?? null,
@@ -114,7 +115,7 @@ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
114
115
  avancarParaShip(raiz, threadId);
115
116
  if (opcoes.publicar !== false)
116
117
  (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
117
- return resultado(threadId, 'registrou', `ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR`, { ...merge, ci });
118
+ return resultado(threadId, 'registrou', (0, locale_1.textoDoCatalogo)((m) => m.ship.prRegistrou(merge.mergeSha.slice(0, 7))), { ...merge, ci });
118
119
  }
119
120
  const ghApi = (caminho) => (0, util_1.exec)('gh', ['api', caminho, '-H', 'Accept: application/vnd.github+json'], process.cwd(), 120000);
120
121
  function lerJson(r) {
@@ -139,25 +140,25 @@ function registrarEntregaExternaPorPr(carregado, threadId, opcoes) {
139
140
  const gh = opcoes.executorGitHub ?? ghApi;
140
141
  const declarados = carregado.manifesto.ci.external_repositories;
141
142
  if (!Object.hasOwn(declarados, repo)) {
142
- return resultado(threadId, 'recusada', `o repositorio ${repo} nao esta declarado em ci.external_repositories do orkastery.yaml`);
143
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prRepoNaoDeclarado(repo)));
143
144
  }
144
145
  if (!Number.isSafeInteger(n) || n < 1)
145
- return resultado(threadId, 'recusada', `--pr precisa ser o numero do PR, recebido ${String(n)}`);
146
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prNumeroInvalido(String(n))));
146
147
  const thread = (0, thread_1.lerThread)(raiz, threadId);
147
148
  if (thread.status === 'fechada')
148
- return resultado(threadId, 'ja-registrada', 'thread ja fechada');
149
+ return resultado(threadId, 'ja-registrada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prThreadFechada));
149
150
  const dir = (0, thread_1.dirThread)(raiz, threadId);
150
151
  if ((0, ledger_1.lerLedger)(dir).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.shipConcluido && e.repositorio === repo && e.pr === n)) {
151
- return resultado(threadId, 'ja-registrada', `o PR #${n} de ${repo} ja tem ship_done`);
152
+ return resultado(threadId, 'ja-registrada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prExternoJaTem(n, repo)));
152
153
  }
153
154
  const pr = lerJson(gh(`repos/${repo}/pulls/${n}`));
154
155
  if (!pr)
155
- return resultado(threadId, 'recusada', `a consulta do PR #${n} de ${repo} ao GitHub falhou`);
156
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prConsultaFalhou(n, repo)));
156
157
  const mergeSha = typeof pr.merge_commit_sha === 'string' && /^[0-9a-f]{40}$/.test(pr.merge_commit_sha) ? pr.merge_commit_sha : null;
157
158
  const headSha = typeof pr.head?.sha === 'string' && /^[0-9a-f]{40}$/.test(pr.head.sha) ? pr.head.sha : null;
158
159
  const base = typeof pr.base?.ref === 'string' && /^[A-Za-z0-9._/-]{1,200}$/.test(pr.base.ref) ? pr.base.ref : null;
159
160
  if (pr.merged !== true || !mergeSha || !headSha || !base) {
160
- return resultado(threadId, 'sem-merge', `o PR #${n} de ${repo} nao esta mesclado`);
161
+ return resultado(threadId, 'sem-merge', (0, locale_1.textoDoCatalogo)((m) => m.ship.prNaoMesclado(n, repo)));
161
162
  }
162
163
  // Achado A4 do CHECK: o PR precisa ser DESTA thread e ter entrado na branch padrao do repositorio. Sem o
163
164
  // vinculo, qualquer PR mesclado do repositorio viraria entrega de qualquer thread; sem a base, um PR
@@ -167,28 +168,28 @@ function registrarEntregaExternaPorPr(carregado, threadId, opcoes) {
167
168
  // letra ou digito colado nao (outro id). Sem diferenciar maiusculas.
168
169
  .some((x) => new RegExp(`(^|[^a-z0-9-])${threadId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`, 'i').test(x));
169
170
  if (!vinculo) {
170
- return resultado(threadId, 'recusada', `o PR #${n} de ${repo} nao cita a thread ${threadId} no titulo, no corpo nem na branch`, { mergeSha, headSha });
171
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prNaoCitaThread(n, repo, threadId)), { mergeSha, headSha });
171
172
  }
172
173
  const repositorio = lerJson(gh(`repos/${repo}`));
173
174
  if (!repositorio)
174
- return resultado(threadId, 'recusada', `a consulta de ${repo} ao GitHub falhou`, { mergeSha, headSha });
175
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prRepoFalhou(repo)), { mergeSha, headSha });
175
176
  if (repositorio.default_branch !== base) {
176
- return resultado(threadId, 'recusada', `o PR #${n} entrou em ${base}, e a branch padrao de ${repo} e ${repositorio.default_branch ?? 'desconhecida'}`, { mergeSha, headSha });
177
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prOutraBase(n, base, repo, repositorio.default_branch)), { mergeSha, headSha });
177
178
  }
178
179
  // A ponta da base AGORA, e o merge dentro dela: o push provado, pela mesma pergunta do ls-remote.
179
180
  const ramo = lerJson(gh(`repos/${repo}/branches/${encodeURIComponent(base)}`));
180
181
  const ponta = typeof ramo?.commit?.sha === 'string' && /^[0-9a-f]{40}$/.test(ramo.commit.sha) ? ramo.commit.sha : '';
181
182
  const comparacao = ponta ? lerJson(gh(`repos/${repo}/compare/${mergeSha}...${ponta}`)) : null;
182
183
  if (!ponta || !comparacao || !['ahead', 'identical'].includes(String(comparacao.status))) {
183
- return resultado(threadId, 'recusada', `o merge ${mergeSha.slice(0, 7)} nao esta na ponta de ${repo}:${base}`, { mergeSha, headSha });
184
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prExternoForaDaPonta(mergeSha.slice(0, 7), repo, base)), { mergeSha, headSha });
184
185
  }
185
186
  const check = declarados[repo];
186
187
  const ci = check ? (0, ci_1.consultarCiDoRepositorio)(repo, headSha, check, opcoes.executorCi) : null;
187
188
  if (ci && !ci.ok) {
188
- return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { mergeSha, headSha, ci });
189
+ return resultado(threadId, 'recusada', (0, locale_1.textoDoCatalogo)((m) => m.ship.prSemCiVerde((0, locale_1.remontar)(ci.detail))), { mergeSha, headSha, ci });
189
190
  }
190
191
  if (opcoes.dryRun) {
191
- return resultado(threadId, 'registraria', `ensaio: registraria ship_done pelo merge ${mergeSha.slice(0, 7)} do PR #${n} de ${repo} (nada gravado)`, { mergeSha, headSha, ci });
192
+ return resultado(threadId, 'registraria', (0, locale_1.textoDoCatalogo)((m) => m.ship.prExternoRegistraria(mergeSha.slice(0, 7), n, repo)), { mergeSha, headSha, ci });
192
193
  }
193
194
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
194
195
  de: thread.base?.branch ?? null,
@@ -215,7 +216,7 @@ function registrarEntregaExternaPorPr(carregado, threadId, opcoes) {
215
216
  avancarParaShip(raiz, threadId);
216
217
  if (opcoes.publicar !== false)
217
218
  (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
218
- return resultado(threadId, 'registrou', `ship_done pelo merge ${mergeSha.slice(0, 7)} do PR #${n} de ${repo}`, { mergeSha, headSha, ci });
219
+ return resultado(threadId, 'registrou', (0, locale_1.textoDoCatalogo)((m) => m.ship.prExternoRegistrou(mergeSha.slice(0, 7), n, repo)), { mergeSha, headSha, ci });
219
220
  }
220
221
  /** `ork ship registrar-pr --todas`: toda thread aberta que ja tem merge `ship(<thread>)` na base. */
221
222
  function registrarEntregasPorPr(carregado, opcoes = {}) {
@@ -230,7 +231,7 @@ function registrarEntregasPorPr(carregado, opcoes = {}) {
230
231
  return registrarEntregaPorPr(carregado, id, { ...opcoes, buscar: false, publicar: false });
231
232
  }
232
233
  catch (e) {
233
- return resultado(id, 'recusada', e.message);
234
+ return resultado(id, 'recusada', e instanceof locale_1.ErroDoCatalogo ? (0, locale_1.textoDoCatalogo)(e.montar) : e.message);
234
235
  }
235
236
  });
236
237
  // Uma publicacao so para o lote inteiro, nao uma por thread.
@@ -60,7 +60,16 @@ function exigirEscopoDeEscrita(threads) {
60
60
  /** Um nome autorizado nao pode ser alias de outra thread ou de estado externo. */
61
61
  function validarDiretorioDeThread(raiz, id) {
62
62
  validarThreadDeEscrita(id);
63
- const dir = (0, estado_thread_1.estadoCanonico)(raiz, id);
63
+ let dir;
64
+ // RM-047: o estadoCanonico já recusa link (estado.link); aqui a recusa mantém o código do escopo.
65
+ try {
66
+ dir = (0, estado_thread_1.estadoCanonico)(raiz, id);
67
+ }
68
+ catch (e) {
69
+ if (e.message.startsWith('estado.link'))
70
+ throw new Error('scope.thread.alias');
71
+ throw e;
72
+ }
64
73
  for (const candidato of [path.dirname(path.dirname(dir)), path.dirname(dir), dir]) {
65
74
  const stat = fs.lstatSync(candidato, { throwIfNoEntry: false });
66
75
  if (!stat)
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.raizDoEstado = raizDoEstado;
37
37
  exports.estadoCanonico = estadoCanonico;
38
+ exports.exigirEstadoSemLink = exigirEstadoSemLink;
38
39
  exports.vincularEstado = vincularEstado;
39
40
  exports.comEstadoParaGit = comEstadoParaGit;
40
41
  exports.auditarEstado = auditarEstado;
@@ -66,7 +67,28 @@ function raizDoEstado(raiz) {
66
67
  function estadoCanonico(raiz, id) {
67
68
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(id))
68
69
  throw new Error('id de thread inválido');
69
- return path.join(raizDoEstado(raiz), '.orkastery', 'threads', id);
70
+ const principal = raizDoEstado(raiz);
71
+ exigirEstadoSemLink(principal, ['.orkastery', 'threads', id]);
72
+ return path.join(principal, '.orkastery', 'threads', id);
73
+ }
74
+ /**
75
+ * RM-047 (fronteira de confiança): na árvore principal, `.orkastery`, `threads` e o diretório da thread
76
+ * são pastas de verdade. Um link versionado no clone levaria ledger, claims e thread.json para fora da
77
+ * raiz, e o `ork` anexaria linhas em arquivo alheio. O link que o próprio `ork` cria fica na worktree
78
+ * (`vincularEstado`) e aponta para cá; ele nunca é um destes componentes.
79
+ */
80
+ function exigirEstadoSemLink(principal, componentes) {
81
+ let atual = principal;
82
+ for (const nome of componentes) {
83
+ atual = path.join(atual, nome);
84
+ const stat = fs.lstatSync(atual, { throwIfNoEntry: false });
85
+ if (!stat)
86
+ return;
87
+ if (stat.isSymbolicLink()) {
88
+ throw new Error(`estado.link: ${path.relative(principal, atual)} é link simbólico; o estado do ork fica em ` +
89
+ 'pastas de verdade dentro da raiz (um link versionado levaria a escrita para fora dela). Remova o link.');
90
+ }
91
+ }
70
92
  }
71
93
  function mesmoArquivo(a, b) {
72
94
  return fs.existsSync(a) && fs.existsSync(b) && fs.realpathSync(a) === fs.realpathSync(b);
@@ -41,10 +41,14 @@ exports.aplicarExperiencia = aplicarExperiencia;
41
41
  const fs = __importStar(require("node:fs"));
42
42
  const path = __importStar(require("node:path"));
43
43
  const node_crypto_1 = require("node:crypto");
44
+ const locale_1 = require("./locale");
44
45
  exports.INICIO_EXPERIENCIA = '<!-- orkastery:experiencia:begin -->';
45
46
  exports.FIM_EXPERIENCIA = '<!-- orkastery:experiencia:end -->';
46
47
  const CONTRATO = 'ork.experiencia-install/v1';
47
- const LINHA_DO_DIRETORIO = /^Se ativo, leia a skill selecionada pelo campo skill no diretório (".*")\.$/;
48
+ /** RM-061 (F3a, D8): as linguas em que o bloco e escrito e, sem recibo, reconhecido. */
49
+ const LOCALES_DO_BLOCO = ['pt-BR', 'en'];
50
+ /** Marca do diretorio no molde da linha: o texto antes e depois dela reconhece a linha em cada lingua. */
51
+ const MARCA_DO_DIRETORIO = '{{diretorio}}';
48
52
  const conflito = () => { throw Error('experiencia.bloco.conflict: bloco ou recibo alterado/duplicado; preserve os arquivos e revise a instalação'); };
49
53
  /** Recusa symlinks e hardlinks em qualquer componente antes da leitura/escrita. */
50
54
  function caminhoSeguro(projeto, arquivo) {
@@ -83,18 +87,35 @@ function decodificar(s) {
83
87
  return b;
84
88
  }
85
89
  const eolDe = (b) => b.includes('\r\n') ? '\r\n' : '\n';
86
- /** O caminho vai relativo ao projeto: o arquivo de instruções costuma ser versionado. */
87
- function nucleoDoBloco(skill, eol) {
88
- return Buffer.from([exports.INICIO_EXPERIENCIA,
89
- '## Experiência de orquestração',
90
- 'Consulte `ork experiencia show --json` neste projeto. Se experience for false, não ative o pacote.',
91
- `Se ativo, leia a skill selecionada pelo campo skill no diretório ${JSON.stringify(skill)}.`,
92
- 'As preferências são dados do núcleo; preserve gates, proveniência humana e permissões do host.',
90
+ /**
91
+ * O caminho vai relativo ao projeto: o arquivo de instruções costuma ser versionado. RM-061 (F3a, D1 e D2): o texto
92
+ * sai na língua pedida, pela família `gerados` do catálogo; os marcadores são os mesmos nas duas línguas.
93
+ */
94
+ function nucleoDoBloco(skill, eol, locale) {
95
+ const t = (0, locale_1.msg)(locale).gerados.experiencia;
96
+ return Buffer.from([exports.INICIO_EXPERIENCIA, `## ${t.titulo}`, t.consulte, t.diretorio(JSON.stringify(skill)), t.preferencias,
93
97
  exports.FIM_EXPERIENCIA, ''].join(eol));
94
98
  }
99
+ /** O diretório escrito na linha do bloco de uma língua, ou null se a linha não é a dessa língua. */
100
+ function diretorioDaLinha(linha, locale) {
101
+ const molde = (0, locale_1.msg)(locale).gerados.experiencia.diretorio(MARCA_DO_DIRETORIO);
102
+ const corte = molde.indexOf(MARCA_DO_DIRETORIO);
103
+ const antes = molde.slice(0, corte), depois = molde.slice(corte + MARCA_DO_DIRETORIO.length);
104
+ if (linha.length < antes.length + depois.length || !linha.startsWith(antes) || !linha.endsWith(depois))
105
+ return null;
106
+ let skill;
107
+ try {
108
+ skill = JSON.parse(linha.slice(antes.length, linha.length - depois.length));
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ return typeof skill === 'string' && !/[\r\n`]/.test(skill) ? skill : null;
114
+ }
95
115
  /**
96
116
  * Bloco sem recibo (clone novo, `.orkastery/` apagado) só é adotado quando é exatamente o texto
97
- * que esta versão ou a anterior geram, com qualquer diretório; qualquer outra edição é conflito.
117
+ * que esta versão ou a anterior geram, com qualquer diretório e em qualquer das duas línguas (RM-061,
118
+ * F3a, D8); qualquer outra edição é conflito.
98
119
  */
99
120
  function reconhecerBloco(antes) {
100
121
  if (contar(antes, exports.INICIO_EXPERIENCIA) !== 1 || contar(antes, exports.FIM_EXPERIENCIA) !== 1)
@@ -108,18 +129,14 @@ function reconhecerBloco(antes) {
108
129
  fim += eol.length;
109
130
  const texto = antes.subarray(inicio, fim);
110
131
  const linha = texto.toString('utf8').split(eol)[3] ?? '';
111
- const m = LINHA_DO_DIRETORIO.exec(linha);
112
- let skill;
113
- try {
114
- skill = m ? JSON.parse(m[1]) : undefined;
115
- }
116
- catch {
117
- return null;
118
- }
119
- if (typeof skill !== 'string' || /[\r\n`]/.test(skill))
120
- return null;
121
- const esperado = nucleoDoBloco(skill, eol);
122
- if (!texto.equals(esperado) && !texto.equals(esperado.subarray(0, esperado.length - eol.length)))
132
+ const reconhecido = LOCALES_DO_BLOCO.some((locale) => {
133
+ const skill = diretorioDaLinha(linha, locale);
134
+ if (skill === null)
135
+ return false;
136
+ const esperado = nucleoDoBloco(skill, eol, locale);
137
+ return texto.equals(esperado) || texto.equals(esperado.subarray(0, esperado.length - eol.length));
138
+ });
139
+ if (!reconhecido)
123
140
  return null;
124
141
  // Uma linha em branco antes do bloco é o separador que a instalação põe depois de uma linha.
125
142
  const sep = Buffer.from(eol);
@@ -144,8 +161,11 @@ function removerBloco(antes, bloco) {
144
161
  const juncao = prefixo.length && prefixo.at(-1) !== 10 && sufixo.length ? Buffer.from(bloco.eol) : Buffer.alloc(0);
145
162
  return Buffer.concat([prefixo, juncao, sufixo]);
146
163
  }
147
- /** Planejamento puro em relação ao disco: dry-run e preflight usam o mesmo contrato. */
148
- function planejarExperiencia(projeto, host, skill) {
164
+ /**
165
+ * Planejamento puro em relação ao disco: dry-run e preflight usam o mesmo contrato. O bloco sai no `locale` pedido
166
+ * (RM-061, F3a), pt-BR por omissão: só o `ork adapter install` passa a língua do dono (D2).
167
+ */
168
+ function planejarExperiencia(projeto, host, skill, locale = locale_1.LOCALE_PADRAO) {
149
169
  projeto = fs.realpathSync(path.resolve(projeto));
150
170
  if (!['claude-code', 'codex'].includes(host))
151
171
  throw Error('experiencia.host.unsupported');
@@ -189,7 +209,7 @@ function planejarExperiencia(projeto, host, skill) {
189
209
  let separador, nucleo, anterior;
190
210
  if (antes && bloco) {
191
211
  // Atualização no lugar: o que está antes e depois do bloco, inclusive edição externa, fica.
192
- nucleo = nucleoDoBloco(skill, bloco.eol);
212
+ nucleo = nucleoDoBloco(skill, bloco.eol, locale);
193
213
  separador = bloco.separador;
194
214
  depois = Buffer.concat([antes.subarray(0, bloco.inicio), nucleo, antes.subarray(bloco.fim)]);
195
215
  // Adotado sem recibo: se só havia o bloco, o arquivo conta como criado pelo pacote.
@@ -198,7 +218,7 @@ function planejarExperiencia(projeto, host, skill) {
198
218
  }
199
219
  else {
200
220
  const base = antes ?? Buffer.alloc(0), eol = eolDe(base);
201
- nucleo = nucleoDoBloco(skill, eol);
221
+ nucleo = nucleoDoBloco(skill, eol, locale);
202
222
  separador = Buffer.from(base.length ? (base.at(-1) === 10 ? eol : eol + eol) : '');
203
223
  depois = Buffer.concat([base, separador, nucleo]);
204
224
  anterior = antes?.toString('base64') ?? null;
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CRONTAB_DO_SISTEMA = exports.MARCA_DA_BATIDA = exports.ultimaRodadaDaVarredura = exports.ATRASO_DA_BATIDA_MIN = void 0;
37
+ exports.gravarCrontabDoSistema = gravarCrontabDoSistema;
38
+ exports.caminhoDaBatida = caminhoDaBatida;
39
+ exports.scriptDaBatida = scriptDaBatida;
40
+ exports.linhaDaBatida = linhaDaBatida;
41
+ exports.linhaGerenciada = linhaGerenciada;
42
+ exports.estadoDaBatida = estadoDaBatida;
43
+ exports.instalarBatida = instalarBatida;
44
+ exports.removerBatida = removerBatida;
45
+ exports.textoDoEstadoDaBatida = textoDoEstadoDaBatida;
46
+ exports.textoDoResultadoDaBatida = textoDoResultadoDaBatida;
47
+ /**
48
+ * RM-063 (F1): a batida da fabrica, instalada e vigiada pelo proprio `ork`.
49
+ *
50
+ * Incidente do maquina-b, de 03/10 a 12/10/2026 (P1 e P7 da pagina da RM-063): o cron da varredura do
51
+ * pulse nunca foi instalado, o `ork doctor` so conferia o crontab com `pulse-host.json` e, sem
52
+ * transporte, calava; a correcao (editar o crontab) foi barrada como persistencia nao autorizada. A
53
+ * maquina ficou nove dias sem batida e ninguem soube.
54
+ *
55
+ * `ork fabrica batida instalar|status|remover` e o comando que o dono autoriza uma vez: instala a linha
56
+ * do template `monitor/pulse.cron` com o checkout canonico e o PATH que acha o `node`, o `claude` e o
57
+ * transporte, preserva as outras entradas do crontab e grava o evento no ledger da maquina
58
+ * (`~/.orkastery/ledger.jsonl`). A linha e marcada (`# ork-batida`) e diz o projeto
59
+ * (`ORK_PULSE_PROJECT`): e por elas que o `remover` tira so a linha dela.
60
+ *
61
+ * O crontab nunca e gravado sem ter sido lido, e a gravacao so vale depois de relida. O executor e
62
+ * injetado: os testes usam um crontab falso e nunca tocam no da maquina.
63
+ */
64
+ const fs = __importStar(require("node:fs"));
65
+ const path = __importStar(require("node:path"));
66
+ const node_child_process_1 = require("node:child_process");
67
+ const doctor_pulse_cron_1 = require("./doctor-pulse-cron");
68
+ var doctor_pulse_cron_2 = require("./doctor-pulse-cron");
69
+ Object.defineProperty(exports, "ATRASO_DA_BATIDA_MIN", { enumerable: true, get: function () { return doctor_pulse_cron_2.ATRASO_DA_BATIDA_MIN; } });
70
+ Object.defineProperty(exports, "ultimaRodadaDaVarredura", { enumerable: true, get: function () { return doctor_pulse_cron_2.ultimaRodadaDaVarredura; } });
71
+ const estado_thread_1 = require("./estado-thread");
72
+ const ledger_1 = require("./ledger");
73
+ const maquina_1 = require("./maquina");
74
+ const horario_1 = require("./horario");
75
+ const util_1 = require("./util");
76
+ const procedencia_1 = require("./procedencia");
77
+ /** O comentario no fim da linha que o `ork` instalou; o `sh` do cron o ignora. */
78
+ exports.MARCA_DA_BATIDA = '# ork-batida';
79
+ /** Os binarios que a varredura chama, alem do `node`: o runtime, a forja e o git. */
80
+ const BINARIOS_DA_VARREDURA = ['claude', 'codex', 'gh', 'git'];
81
+ const PATH_DO_SISTEMA = ['/usr/local/bin', '/usr/bin', '/bin'];
82
+ /** `crontab -` com o texto no stdin, sem shell e com prazo. */
83
+ function gravarCrontabDoSistema(texto) {
84
+ const r = (0, node_child_process_1.spawnSync)('crontab', ['-'], { input: texto, encoding: 'utf8', timeout: 5000 });
85
+ if (r.status === 0)
86
+ return { ok: true };
87
+ const erro = r.error ? r.error.code ?? r.error.message : null;
88
+ if (erro === 'ENOENT')
89
+ return { ok: false, motivo: 'comando crontab ausente nesta maquina' };
90
+ const primeira = `${r.stderr ?? ''}${r.stdout ?? ''}`.trim().split('\n')[0] ?? '';
91
+ return { ok: false, motivo: primeira ? `crontab - falhou: ${primeira}` : `crontab - saiu ${r.status ?? erro ?? 'sem codigo'}` };
92
+ }
93
+ exports.CRONTAB_DO_SISTEMA = { ler: doctor_pulse_cron_1.lerCrontabDoSistema, gravar: gravarCrontabDoSistema };
94
+ /** Caminho que entra na linha do cron: absoluto, sem `%` (o cron o troca por quebra de linha) e sem controle. */
95
+ function caminhoAceito(caminho) {
96
+ return path.isAbsolute(caminho) && !/[%\p{Cc}]/u.test(caminho);
97
+ }
98
+ /** Diretorio aceito no PATH da linha: alem do caminho aceito, sem `:`, o separador do PATH. */
99
+ function diretorioAceito(dir) {
100
+ return caminhoAceito(dir) && !dir.includes(':');
101
+ }
102
+ /**
103
+ * O executavel do transporte, so do `pulse-host.json` local desta maquina (RM-047, P2): o versionado no git, ou o link,
104
+ * nao poe diretorio nenhum no PATH da batida.
105
+ */
106
+ function transporteDoProjeto(raiz, dirMonitor) {
107
+ const arquivo = path.join(dirMonitor, 'pulse-host.json');
108
+ try {
109
+ (0, procedencia_1.exigirHostLocal)(raiz, arquivo);
110
+ const config = JSON.parse(fs.readFileSync(arquivo, 'utf8'));
111
+ return typeof config.executavel === 'string' && config.executavel.trim() ? config.executavel.trim() : null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * O PATH da linha: o diretorio do `node` que roda este `ork`, os do `claude`, `codex`, `gh` e `git` achados
119
+ * agora, o do executavel do transporte (`pulse-host.json`) e o PATH minimo do sistema. O cron nao carrega o
120
+ * perfil do shell; sem isto a varredura nao acha o runtime nem o transporte (o template avisa).
121
+ */
122
+ function caminhoDaBatida(opcoes = {}) {
123
+ const achar = opcoes.achar ?? ((bin) => (0, util_1.noPath)(bin));
124
+ const dirs = [];
125
+ const somar = (arquivo) => {
126
+ if (!arquivo || !path.isAbsolute(arquivo))
127
+ return;
128
+ const dir = path.dirname(arquivo);
129
+ if (diretorioAceito(dir) && !dirs.includes(dir))
130
+ dirs.push(dir);
131
+ };
132
+ somar(opcoes.node ?? process.execPath);
133
+ for (const bin of BINARIOS_DA_VARREDURA)
134
+ somar(achar(bin));
135
+ if (opcoes.transporte)
136
+ somar(path.isAbsolute(opcoes.transporte) ? opcoes.transporte : achar(opcoes.transporte));
137
+ for (const dir of PATH_DO_SISTEMA)
138
+ if (!dirs.includes(dir))
139
+ dirs.push(dir);
140
+ return dirs.join(':');
141
+ }
142
+ /** O script da varredura que a linha roda: o do checkout do projeto, senao o do `ork` instalado. */
143
+ function scriptDaBatida(raiz, scriptDoOrk = (0, doctor_pulse_cron_1.scriptDaVarreduraDoOrk)()) {
144
+ const doProjeto = path.join(raiz, 'monitor', doctor_pulse_cron_1.SCRIPT_DA_VARREDURA);
145
+ return fs.existsSync(doProjeto) ? doProjeto : scriptDoOrk;
146
+ }
147
+ /**
148
+ * A linha que o `ork` instala: a batida do template (`*\/15`), o projeto alvo, o PATH e o script, com a
149
+ * marca no fim. `raiz` e a raiz canonica (o checkout principal, nunca a worktree de uma thread).
150
+ */
151
+ function linhaDaBatida(raiz, opcoes = {}) {
152
+ const script = scriptDaBatida(raiz, opcoes.scriptDoOrk === undefined ? (0, doctor_pulse_cron_1.scriptDaVarreduraDoOrk)() : opcoes.scriptDoOrk);
153
+ if (!script) {
154
+ throw new Error(`fabrica.batida.sem-script: nem ${path.join(raiz, 'monitor', doctor_pulse_cron_1.SCRIPT_DA_VARREDURA)} nem o ` +
155
+ `${doctor_pulse_cron_1.SCRIPT_DA_VARREDURA} do ork instalado existem; reinstale o ork (npm i -g @orkastery/cli)`);
156
+ }
157
+ for (const caminho of [raiz, script]) {
158
+ if (!caminhoAceito(caminho)) {
159
+ throw new Error(`fabrica.batida.caminho-invalido: ${JSON.stringify(caminho)} nao cabe numa linha do cron ` +
160
+ '(precisa ser absoluto, sem % e sem caractere de controle)');
161
+ }
162
+ }
163
+ const PATH = caminhoDaBatida({ node: opcoes.node, achar: opcoes.achar,
164
+ transporte: transporteDoProjeto(raiz, path.join(raiz, '.orkastery', 'monitor')) });
165
+ return `${doctor_pulse_cron_1.BATIDA_DO_TEMPLATE} ORK_PULSE_PROJECT=${(0, doctor_pulse_cron_1.citarNoShell)(raiz)} PATH=${(0, doctor_pulse_cron_1.citarNoShell)(PATH)} ` +
166
+ `${(0, doctor_pulse_cron_1.citarNoShell)(script)} ${exports.MARCA_DA_BATIDA}`;
167
+ }
168
+ /** A linha foi instalada pelo `ork fabrica batida instalar` (traz a marca no fim)? */
169
+ function linhaGerenciada(linha) {
170
+ return linha.trimEnd().endsWith(exports.MARCA_DA_BATIDA);
171
+ }
172
+ /**
173
+ * As entradas da varredura DESTE projeto, separadas pela marca. O projeto efetivo considera a linha de ambiente do
174
+ * crontab acima da entrada: o template abaixo de `ORK_PULSE_PROJECT=/outro` e a batida do outro, e fica.
175
+ */
176
+ function separar(texto, raiz) {
177
+ const doProjeto = (0, doctor_pulse_cron_1.entradasDaVarredura)(texto).filter((e) => (0, doctor_pulse_cron_1.entradaDoProjeto)(e, raiz)).map((e) => e.linha);
178
+ return { gerenciadas: doProjeto.filter(linhaGerenciada), manuais: doProjeto.filter((l) => !linhaGerenciada(l)) };
179
+ }
180
+ function ritmoDa(linha) {
181
+ const t = linha.trim();
182
+ const m = t.startsWith('@') ? /^(@\S+)\s/.exec(t) : /^(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s/.exec(t);
183
+ return m ? (0, doctor_pulse_cron_1.intervaloEmMinutos)(m[1]) : null;
184
+ }
185
+ /** `ork fabrica batida status`: o que o crontab desta maquina diz da batida deste projeto, e a ultima rodada. */
186
+ function estadoDaBatida(raizDoProjeto, opcoes = {}) {
187
+ const raiz = (0, estado_thread_1.raizDoEstado)(raizDoProjeto);
188
+ const executor = opcoes.executor ?? exports.CRONTAB_DO_SISTEMA;
189
+ const quando = opcoes.quando ?? new Date().toISOString();
190
+ const leitura = executor.ler();
191
+ const { gerenciadas, manuais } = leitura.ok ? separar(leitura.texto, raiz) : { gerenciadas: [], manuais: [] };
192
+ const ritmos = [...gerenciadas, ...manuais].map(ritmoDa).filter((r) => r !== null);
193
+ const rodada = (0, doctor_pulse_cron_1.ultimaRodadaDaVarredura)(path.join(raiz, '.orkastery', 'monitor'));
194
+ return {
195
+ projeto: raiz, maquina: (0, maquina_1.nomeDaMaquina)(),
196
+ leitura: leitura.ok ? { ok: true } : { ok: false, motivo: leitura.motivo },
197
+ gerenciada: gerenciadas[0]?.trim() ?? null, manuais: manuais.map((l) => l.trim()),
198
+ instalada: gerenciadas.length + manuais.length > 0,
199
+ intervaloMin: ritmos.length ? Math.min(...ritmos) : null,
200
+ ultimaRodada: rodada ? { ...rodada, haMin: Math.max(0, Math.floor((Date.parse(quando) - Date.parse(rodada.em)) / 60000)) } : null,
201
+ };
202
+ }
203
+ function lerParaEditar(executor) {
204
+ const leitura = executor.ler();
205
+ // Nunca gravar sem ter lido: um crontab que nao foi lido seria apagado pela gravacao.
206
+ if (!leitura.ok)
207
+ throw new Error(`fabrica.batida.sem-leitura: ${leitura.motivo}; nada foi gravado`);
208
+ return leitura.texto;
209
+ }
210
+ function gravarEConferir(executor, lido, texto, confere) {
211
+ // O crontab que mudou entre a leitura e a gravacao (um `crontab -e`, outro `instalar`) nao e sobrescrito.
212
+ const antes = executor.ler();
213
+ if (!antes.ok || antes.texto !== lido) {
214
+ throw new Error('fabrica.batida.concorrencia: o crontab mudou durante a operacao; nada foi gravado, rode de novo');
215
+ }
216
+ const gravacao = executor.gravar(texto);
217
+ if (!gravacao.ok)
218
+ throw new Error(`fabrica.batida.sem-gravacao: ${gravacao.motivo}`);
219
+ const relido = executor.ler();
220
+ if (!relido.ok || !confere(relido.texto)) {
221
+ throw new Error(`fabrica.batida.nao-confirmada: o crontab relido nao traz o que foi gravado` +
222
+ (relido.ok ? '' : ` (${relido.motivo})`) + '; confira com crontab -l');
223
+ }
224
+ }
225
+ /** O texto do crontab sem as linhas pedidas, com o fim de linha que o cron exige. */
226
+ function semAsLinhas(texto, sair) {
227
+ const ficam = texto.split('\n').filter((l) => !sair.has(l));
228
+ while (ficam.length && ficam[ficam.length - 1].trim() === '')
229
+ ficam.pop();
230
+ return ficam.length ? `${ficam.join('\n')}\n` : '';
231
+ }
232
+ function noLedgerDaMaquina(tipo, dados) {
233
+ return (0, ledger_1.registrar)((0, maquina_1.pastaDoUsuario)(), 'maquina', tipo, { maquina: (0, maquina_1.nomeDaMaquina)(), ...dados }).eventId;
234
+ }
235
+ /**
236
+ * `ork fabrica batida instalar`: idempotente. A linha igual ja instalada nao muda nada; a linha antiga do
237
+ * `ork` e a linha posta a mao da varredura deste projeto saem, e a linha do template entra no lugar. As
238
+ * outras entradas do crontab ficam como estavam.
239
+ */
240
+ function instalarBatida(raizDoProjeto, opcoes = {}) {
241
+ const raiz = (0, estado_thread_1.raizDoEstado)(raizDoProjeto);
242
+ const executor = opcoes.executor ?? exports.CRONTAB_DO_SISTEMA;
243
+ const linha = linhaDaBatida(raiz, opcoes);
244
+ const texto = lerParaEditar(executor);
245
+ const { gerenciadas, manuais } = separar(texto, raiz);
246
+ if (manuais.length === 0 && gerenciadas.length === 1 && gerenciadas[0].trim() === linha) {
247
+ return { acao: 'ja-instalada', projeto: raiz, linha, retiradas: [], manuais: [], eventId: null };
248
+ }
249
+ const retiradas = [...gerenciadas, ...manuais];
250
+ const base = semAsLinhas(texto, new Set(retiradas));
251
+ gravarEConferir(executor, texto, `${base}${linha}\n`, (relido) => relido.split('\n').some((l) => l.trim() === linha));
252
+ const eventId = noLedgerDaMaquina(ledger_1.TIPOS_DE_EVENTO.batidaInstalada, {
253
+ projeto: raiz, linha, retiradas: retiradas.map((l) => l.trim()), por: opcoes.por ?? null,
254
+ evidencia: 'crontab -l relido depois da gravacao traz a linha',
255
+ });
256
+ return { acao: 'instalou', projeto: raiz, linha, retiradas: retiradas.map((l) => l.trim()), manuais: [], eventId };
257
+ }
258
+ /** `ork fabrica batida remover`: tira so a linha que o `ork` instalou para este projeto. */
259
+ function removerBatida(raizDoProjeto, opcoes = {}) {
260
+ const raiz = (0, estado_thread_1.raizDoEstado)(raizDoProjeto);
261
+ const executor = opcoes.executor ?? exports.CRONTAB_DO_SISTEMA;
262
+ const texto = lerParaEditar(executor);
263
+ const { gerenciadas, manuais } = separar(texto, raiz);
264
+ const ficam = manuais.map((l) => l.trim());
265
+ if (gerenciadas.length === 0)
266
+ return { acao: 'nao-instalada', projeto: raiz, linha: null, retiradas: [], manuais: ficam, eventId: null };
267
+ const sair = new Set(gerenciadas);
268
+ gravarEConferir(executor, texto, semAsLinhas(texto, sair), (relido) => separar(relido, raiz).gerenciadas.length === 0);
269
+ const eventId = noLedgerDaMaquina(ledger_1.TIPOS_DE_EVENTO.batidaRemovida, {
270
+ projeto: raiz, retiradas: gerenciadas.map((l) => l.trim()), manuaisQueFicaram: ficam, por: opcoes.por ?? null,
271
+ evidencia: 'crontab -l relido depois da gravacao nao traz a linha do ork',
272
+ });
273
+ return { acao: 'removeu', projeto: raiz, linha: null, retiradas: gerenciadas.map((l) => l.trim()), manuais: ficam, eventId };
274
+ }
275
+ function textoDoEstadoDaBatida(e, quando = new Date().toISOString()) {
276
+ const linhas = [`Batida da fábrica em ${e.maquina}, projeto ${e.projeto}`];
277
+ if (!e.leitura.ok)
278
+ linhas.push(` crontab: não lido (${e.leitura.motivo}); não dá para dizer se a batida está instalada`);
279
+ else if (!e.instalada)
280
+ linhas.push(' instalada: não; instale com: ork fabrica batida instalar');
281
+ else {
282
+ const ritmo = e.intervaloMin === null ? 'com horário que o ork não lê' : `a cada ${e.intervaloMin} min`;
283
+ linhas.push(` instalada: sim, ${ritmo}${e.gerenciada ? ', pelo ork fabrica batida instalar' : ', por linha posta à mão'}`);
284
+ for (const l of [...(e.gerenciada ? [e.gerenciada] : []), ...e.manuais])
285
+ linhas.push(` linha: ${l}`);
286
+ }
287
+ const r = e.ultimaRodada;
288
+ if (!r)
289
+ linhas.push(' última rodada: nenhuma registrada (.orkastery/monitor/hitl.log)');
290
+ else {
291
+ linhas.push(` última rodada: ${(0, horario_1.formatarHora)(r.em, { agora: quando })} (há ${r.haMin} min), ` +
292
+ `${r.code === 0 ? 'ok' : `código ${r.code ?? '?'}`}: ${r.detalhe || 'sem detalhe'}`);
293
+ if (e.instalada && r.haMin > doctor_pulse_cron_1.ATRASO_DA_BATIDA_MIN) {
294
+ linhas.push(` atrasada: nenhuma rodada há ${r.haMin} min com a batida instalada; confira crontab -l e o hitl.log`);
295
+ }
296
+ }
297
+ linhas.push((0, horario_1.legendaDoFuso)());
298
+ return linhas.join('\n');
299
+ }
300
+ function textoDoResultadoDaBatida(r) {
301
+ switch (r.acao) {
302
+ case 'ja-instalada': return `Batida já instalada para ${r.projeto}; nada mudou no crontab.\n linha: ${r.linha}`;
303
+ case 'instalou': return [`Batida instalada para ${r.projeto} (evento ${r.eventId} no ledger da máquina).`, ` linha: ${r.linha}`,
304
+ ...r.retiradas.map((l) => ` saiu: ${l}`), ' confira com: ork fabrica batida status'].join('\n');
305
+ case 'removeu': return [`Batida removida de ${r.projeto} (evento ${r.eventId} no ledger da máquina).`,
306
+ ...r.retiradas.map((l) => ` saiu: ${l}`), ...r.manuais.map((l) => ` ficou (posta à mão, não é do ork): ${l}`)].join('\n');
307
+ case 'nao-instalada': return [`Nenhuma batida do ork instalada para ${r.projeto}; nada mudou no crontab.`,
308
+ ...r.manuais.map((l) => ` ficou (posta à mão, não é do ork): ${l}`)].join('\n');
309
+ }
310
+ }