@orkastery/cli 0.4.2 → 0.5.0

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 (128) hide show
  1. package/adapters/README.md +11 -0
  2. package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
  3. package/adapters/claude-code/agents/ork-check.md +1 -1
  4. package/adapters/claude-code/agents/ork-go.md +2 -2
  5. package/adapters/claude-code/agents/ork-goal.md +1 -1
  6. package/adapters/claude-code/agents/ork-master.md +1 -1
  7. package/adapters/claude-code/agents/ork-plan.md +1 -1
  8. package/adapters/claude-code/agents/ork-ship.md +1 -1
  9. package/adapters/claude-code/commands/master.md +11 -8
  10. package/adapters/claude-code/commands/ork.md +8 -0
  11. package/adapters/claude-code/commands/ship.md +2 -2
  12. package/adapters/codex/skills/ork/SKILL.md +12 -1
  13. package/adapters/hermes/README.md +3 -0
  14. package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
  15. package/adapters/hermes/bin/ork-brain.sh +3 -0
  16. package/adapters/hermes/bin/ork-maestro.sh +4 -1
  17. package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
  18. package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
  19. package/adapters/hermes/hermes.plugin.json +2 -0
  20. package/adapters/hermes/hitl-ingress/__init__.py +36 -3
  21. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
  22. package/adapters/openclaw/README.md +34 -3
  23. package/adapters/openclaw/dist/hitl-ingress.js +37 -3
  24. package/adapters/openclaw/dist/index.js +92 -39
  25. package/adapters/openclaw/openclaw.plugin.json +10 -3
  26. package/adapters/openclaw/package.json +2 -2
  27. package/adapters/openclaw/src/hitl-ingress.ts +32 -3
  28. package/adapters/openclaw/src/index.ts +108 -41
  29. package/assets/docs/padroes/documentacao-de-produto.md +2 -2
  30. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  31. package/assets/docs/produto/_modelo-feature.md +2 -0
  32. package/assets/orkmind_bridge.py +214 -1
  33. package/dist/adapters/claude-bg.js +88 -9
  34. package/dist/adapters/codex-events.js +3 -1
  35. package/dist/board.js +74 -0
  36. package/dist/branch-de-estado.js +5 -4
  37. package/dist/busca-semantica.js +161 -0
  38. package/dist/ci.js +141 -10
  39. package/dist/company-brain-cli.js +27 -2
  40. package/dist/company-brain-context.js +132 -0
  41. package/dist/company-brain-dossie.js +376 -0
  42. package/dist/company-brain-mcp.js +4 -1
  43. package/dist/conducao.js +117 -31
  44. package/dist/decisao-autonoma.js +42 -0
  45. package/dist/docs.js +64 -22
  46. package/dist/doctor.js +33 -0
  47. package/dist/entrega-pr.js +109 -5
  48. package/dist/experiencia-instalacao.js +256 -0
  49. package/dist/experiencia.js +72 -0
  50. package/dist/fabrica-estado.js +43 -10
  51. package/dist/fechamento.js +105 -0
  52. package/dist/fix.js +1 -1
  53. package/dist/forja.js +435 -0
  54. package/dist/gates.js +10 -1
  55. package/dist/hitl-classificacao.js +11 -1
  56. package/dist/hitl-contract.js +50 -7
  57. package/dist/hitl-curto.js +158 -0
  58. package/dist/hitl-gates.js +67 -1
  59. package/dist/hitl-lote.js +30 -15
  60. package/dist/hitl-presentation.js +10 -7
  61. package/dist/hitl-resumo.js +33 -12
  62. package/dist/hitl-texto-livre.js +150 -0
  63. package/dist/hosts.js +86 -1
  64. package/dist/index.js +605 -41
  65. package/dist/indice-vetorial.js +317 -0
  66. package/dist/init.js +13 -0
  67. package/dist/integracoes-locais.js +1 -1
  68. package/dist/intelligence-benchmark-contract.js +570 -0
  69. package/dist/intelligence-graph-cli.js +430 -0
  70. package/dist/intelligence-graph-contract.js +577 -0
  71. package/dist/intelligence-graph-extract-md.js +469 -0
  72. package/dist/intelligence-graph-extract-ts.js +810 -0
  73. package/dist/intelligence-graph-extract.js +369 -0
  74. package/dist/intelligence-graph-index.js +512 -0
  75. package/dist/intelligence-graph-parsers.js +327 -0
  76. package/dist/intelligence-graph-query.js +344 -0
  77. package/dist/intelligence-graph-repo.js +187 -0
  78. package/dist/leases.js +84 -0
  79. package/dist/ledger.js +14 -0
  80. package/dist/maestro-cli.js +10 -1
  81. package/dist/maestro-contract.js +5 -1
  82. package/dist/maestro-discovery.js +16 -2
  83. package/dist/maestro-snapshot.js +6 -2
  84. package/dist/manifest.js +142 -5
  85. package/dist/master-batch.js +3 -2
  86. package/dist/master-digest.js +2 -2
  87. package/dist/master-nota.js +203 -0
  88. package/dist/master.js +4 -0
  89. package/dist/mcp-experiencia.js +164 -0
  90. package/dist/mcp-git.js +36 -10
  91. package/dist/mcp-install.js +3 -1
  92. package/dist/mcp-maestro.js +3 -2
  93. package/dist/mcp-server.js +84 -5
  94. package/dist/memoria.js +147 -1
  95. package/dist/network-roadmap.js +907 -0
  96. package/dist/ocupacao.js +38 -2
  97. package/dist/onboarding.js +113 -7
  98. package/dist/orkmind.js +165 -9
  99. package/dist/phase.js +166 -13
  100. package/dist/policies.js +2 -0
  101. package/dist/projeto-alvo.js +551 -0
  102. package/dist/prompts.js +1 -1
  103. package/dist/pulse-consentimento.js +4 -2
  104. package/dist/pulse-delivery.js +5 -0
  105. package/dist/pulse-resposta.js +674 -49
  106. package/dist/pulse.js +21 -0
  107. package/dist/ratelimit.js +3 -2
  108. package/dist/retry.js +230 -9
  109. package/dist/roadmap-reservas.js +262 -20
  110. package/dist/roadmap-status.js +213 -0
  111. package/dist/runtime-ambiente.js +12 -1
  112. package/dist/session-watcher-claude.js +26 -0
  113. package/dist/sessoes.js +49 -15
  114. package/dist/thread-close.js +3 -0
  115. package/dist/verify.js +5 -4
  116. package/dist/worktree.js +77 -0
  117. package/eval/casos/orchestration-experience-pt-br.json +48 -0
  118. package/eval/casos/orchestration-experience.json +48 -0
  119. package/package.json +1 -1
  120. package/schemas/code-artifact-graph.v1.schema.json +581 -0
  121. package/schemas/graph-benchmark.v1.schema.json +2240 -0
  122. package/schemas/maestro-snapshot.schema.json +23 -0
  123. package/skills/README.md +7 -5
  124. package/skills/core/onboarding/SKILL.md +7 -0
  125. package/skills/core/orchestration-experience/SKILL.md +113 -0
  126. package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
  127. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  128. package/skills/phases/go-implementation/SKILL.md +2 -0
@@ -39,7 +39,43 @@ const hitl_gates_1 = require("./hitl-gates");
39
39
  const ledger_1 = require("./ledger");
40
40
  const thread_1 = require("./thread");
41
41
  const modos_1 = require("./modos");
42
+ const conducao_1 = require("./conducao");
42
43
  exports.CONTRATO_DECISAO_AUTONOMA = 'ork.decisao-autonoma/v1';
44
+ /**
45
+ * A sessao de OUTRA thread que registra aqui tambem deixa rastro (S-3 do CHECK 3). So o par que o ledger
46
+ * daquela thread confirma (S-b do CHECK 4): o par vazado do daemon do `claude --bg` nao vira rastro falso.
47
+ */
48
+ function despachoDeOutraThread(raiz, threadId) {
49
+ const thread = (process.env[conducao_1.ENV_THREAD_DO_DESPACHO] ?? '').trim(), id = (process.env[conducao_1.ENV_IDENTIDADE_DE_DESPACHO] ?? '').trim();
50
+ if (!thread || thread === threadId || !/^[a-z0-9]{1,3}-[a-z0-9]{1,12}$/.test(thread) || !/^[a-f0-9-]{36}$/.test(id))
51
+ return {};
52
+ // R5-A2 do CHECK 5: a mesma conferencia D-4 da reentrada. O par que o daemon do `claude --bg` vazou e um
53
+ // despacho real, que o ledger confirma; so a sessao que o recebeu de fato o valida.
54
+ let confirmado = false;
55
+ try {
56
+ confirmado = (0, conducao_1.identidadeDoAmbiente)(thread, process.env, raiz) === id;
57
+ }
58
+ catch { /* thread de outro projeto ou ilegivel: sem prova, sem rastro */ }
59
+ if (confirmado) {
60
+ try {
61
+ confirmado = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread)).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.faseDespachada &&
62
+ e.identidade?.dispatchId === id);
63
+ }
64
+ catch {
65
+ confirmado = false;
66
+ }
67
+ }
68
+ return confirmado ? { despachoNoAmbiente: { thread, dispatchId: id } } : {};
69
+ }
70
+ /** O canal do processo; canal fora do registro nao impede a decisao, e fica dito como tal. */
71
+ function canalSeguro() {
72
+ try {
73
+ return (0, conducao_1.canalDoProcesso)();
74
+ }
75
+ catch {
76
+ return 'desconhecido';
77
+ }
78
+ }
43
79
  /** D3/M7: o p90 das 155 fases ja medidas. Acima disso a fase esta no decil mais alto do projeto. */
44
80
  exports.LIMIAR_DE_DECISOES_POR_FASE = 13;
45
81
  /** Os nomes que o rastro teve antes de ser tipado (M6), na ordem em que valem na leitura. */
@@ -110,6 +146,12 @@ function registrarDecisao(raiz, threadId, entrada) {
110
146
  const evento = (0, ledger_1.registrar)(dir, t.id, ledger_1.TIPOS_DE_EVENTO.decisaoAutonoma, {
111
147
  contratoDecisao: exports.CONTRATO_DECISAO_AUTONOMA, fase: pedido.fase, decisao: pedido.decidido, ...rastro,
112
148
  pedido, ...(entrada.reverte ? { reverte: entrada.reverte } : {}),
149
+ origem: entrada.origem ?? 'cli', ...(entrada.host ? { host: entrada.host } : {}),
150
+ // RM-037 (S5 do CHECK): a porta e quem chamou, pelo processo e nao pelo texto de `quemDecidiu`. A sessao
151
+ // despachada carrega a identidade do despacho no ambiente; o dono no terminal, nao.
152
+ canal: entrada.origem === 'mcp' && entrada.host ? entrada.host : canalSeguro(),
153
+ despacho: entrada.despacho !== undefined ? entrada.despacho : (0, conducao_1.identidadeDoAmbiente)(t.id, process.env, raiz),
154
+ ...despachoDeOutraThread(raiz, t.id),
113
155
  });
114
156
  return { pedido, evento };
115
157
  });
package/dist/docs.js CHANGED
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LIMITE_DO_PARAGRAFO = exports.LIMITE_DA_FRASE = exports.CATEGORIAS = exports.DIMENSOES = exports.ESTADOS_DA_ESPECIFICACAO = exports.DIR_PADROES = exports.DIR_ROADMAP = exports.DIR_PRODUTO = void 0;
37
37
  exports.separarFrontmatter = separarFrontmatter;
38
+ exports.ehPaginaDeDocs = ehPaginaDeDocs;
39
+ exports.documentoDeTexto = documentoDeTexto;
38
40
  exports.carregarDocs = carregarDocs;
39
41
  exports.resolverBase = resolverBase;
40
42
  exports.comandosDaAjuda = comandosDaAjuda;
@@ -44,6 +46,7 @@ exports.renderizarEstado = renderizarEstado;
44
46
  exports.textoDaVerificacao = textoDaVerificacao;
45
47
  exports.escreverYaml = escreverYaml;
46
48
  exports.mergeDaThread = mergeDaThread;
49
+ exports.escopoPadraoDoSync = escopoPadraoDoSync;
47
50
  exports.sincronizarDocs = sincronizarDocs;
48
51
  exports.tabelaDoRoadmap = tabelaDoRoadmap;
49
52
  exports.tabelaDoProduto = tabelaDoProduto;
@@ -136,38 +139,49 @@ function separarFrontmatter(texto) {
136
139
  return { bruto: null, corpo: t };
137
140
  return { bruto: t.slice(4, fim + 1), corpo: t.slice(fim + 5) };
138
141
  }
142
+ /** `_modelo-*.md` e `README.md` sao modelo e indice, nao paginas de entidade. */
143
+ function ehPaginaDeDocs(nome) {
144
+ return nome.endsWith('.md') && !nome.startsWith('_') && nome !== 'README.md';
145
+ }
139
146
  function paginas(raiz, dir) {
140
147
  const abs = path.join(raiz, dir);
141
148
  if (!fs.existsSync(abs))
142
149
  return [];
143
- // `_modelo-*.md` e `README.md` sao modelo e indice, nao paginas de entidade.
144
150
  return fs.readdirSync(abs)
145
- .filter((n) => n.endsWith('.md') && !n.startsWith('_') && n !== 'README.md')
151
+ .filter(ehPaginaDeDocs)
146
152
  .sort()
147
153
  .map((n) => `${dir}/${n}`);
148
154
  }
155
+ /**
156
+ * RM-054: uma pagina a partir do texto, venha ele do disco, de um blob do git ou da forja. O
157
+ * achado e o mesmo que a leitura do disco daria.
158
+ */
159
+ function documentoDeTexto(arquivo, texto) {
160
+ const { bruto, corpo } = separarFrontmatter(texto);
161
+ if (bruto === null) {
162
+ return { achado: erro(arquivo, undefined, 'docs.frontmatter.ausente', 'página sem frontmatter YAML', 'comece o arquivo com as chaves do modelo entre duas linhas ---') };
163
+ }
164
+ let dados;
165
+ try {
166
+ dados = (0, yaml_1.lerYaml)(bruto);
167
+ }
168
+ catch (e) {
169
+ return { achado: erro(arquivo, undefined, 'docs.frontmatter.invalido', `frontmatter inválido: ${e.message}`) };
170
+ }
171
+ if (!ehMapa(dados)) {
172
+ return { achado: erro(arquivo, undefined, 'docs.frontmatter.invalido', 'o frontmatter precisa ser um mapa de chaves') };
173
+ }
174
+ return { doc: { arquivo, tipo: dados.tipo, id: String(dados.id ?? ''), dados, corpo } };
175
+ }
149
176
  function carregarDocs(raiz) {
150
177
  const docs = [];
151
178
  const achados = [];
152
179
  for (const arquivo of [...paginas(raiz, exports.DIR_PRODUTO), ...paginas(raiz, exports.DIR_ROADMAP)]) {
153
- const { bruto, corpo } = separarFrontmatter(fs.readFileSync(path.join(raiz, arquivo), 'utf8'));
154
- if (bruto === null) {
155
- achados.push(erro(arquivo, undefined, 'docs.frontmatter.ausente', 'página sem frontmatter YAML', 'comece o arquivo com as chaves do modelo entre duas linhas ---'));
156
- continue;
157
- }
158
- let dados;
159
- try {
160
- dados = (0, yaml_1.lerYaml)(bruto);
161
- }
162
- catch (e) {
163
- achados.push(erro(arquivo, undefined, 'docs.frontmatter.invalido', `frontmatter inválido: ${e.message}`));
164
- continue;
165
- }
166
- if (!ehMapa(dados)) {
167
- achados.push(erro(arquivo, undefined, 'docs.frontmatter.invalido', 'o frontmatter precisa ser um mapa de chaves'));
168
- continue;
169
- }
170
- docs.push({ arquivo, tipo: dados.tipo, id: String(dados.id ?? ''), dados, corpo });
180
+ const lido = documentoDeTexto(arquivo, fs.readFileSync(path.join(raiz, arquivo), 'utf8'));
181
+ if ('doc' in lido)
182
+ docs.push(lido.doc);
183
+ else
184
+ achados.push(lido.achado);
171
185
  }
172
186
  return { docs, achados };
173
187
  }
@@ -664,12 +678,40 @@ function temBranch(raiz, thread) {
664
678
  return r.ok && r.saida.length > 0;
665
679
  }
666
680
  const ORDEM_DO_CODIGO = exports.DIMENSOES.codigo;
681
+ /**
682
+ * RM-037 (rm037noite, defeito 5): o escopo padrao do `ork docs sincronizar`. Na worktree de uma thread
683
+ * com item do roadmap, so o item dela: da worktree da ork-rm037noite, o sincronizar gravava RM-025,
684
+ * 026, 031, 038, 048, 049, 050 e 054, e cada PR conflitava com as outras. Fora de worktree de thread
685
+ * (a raiz, onde o condutor sincroniza depois do merge), todo item, como antes.
686
+ */
687
+ function escopoPadraoDoSync(raiz) {
688
+ const real = (p) => { try {
689
+ return fs.realpathSync(p);
690
+ }
691
+ catch {
692
+ return path.resolve(p);
693
+ } };
694
+ const aqui = real(raiz);
695
+ for (const id of (0, thread_1.listarIds)(raiz)) {
696
+ let t;
697
+ try {
698
+ t = (0, thread_1.lerThread)(raiz, id);
699
+ }
700
+ catch {
701
+ continue;
702
+ }
703
+ if (t.worktree && real(t.worktree) === aqui)
704
+ return { itens: t.roadmap ? [t.roadmap] : null, thread: t.id };
705
+ }
706
+ return { itens: null, thread: null };
707
+ }
667
708
  function sincronizarDocs(raiz, opcoes = {}) {
668
709
  const base = resolverBase(raiz, opcoes.baseBranch) ?? (opcoes.baseBranch ?? 'main');
669
710
  const agora = opcoes.agora ?? (() => isoComFusoLocal(new Date()));
670
711
  const { docs } = carregarDocs(raiz);
671
712
  const mudancas = [];
672
- for (const d of docs.filter((x) => x.tipo === 'roadmap')) {
713
+ const noEscopo = (d) => !opcoes.itens || opcoes.itens.includes(d.id);
714
+ for (const d of docs.filter((x) => x.tipo === 'roadmap' && noEscopo(x))) {
673
715
  const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
674
716
  const thread = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread : null;
675
717
  if (!thread)
@@ -715,7 +757,7 @@ function sincronizarDocs(raiz, opcoes = {}) {
715
757
  }
716
758
  // As tabelas de estado do corpo sao vistas do frontmatter: regeradas em todo item de roadmap,
717
759
  // inclusive quando a pessoa mudou o frontmatter a mao (ciclo, deploy, exposicao...).
718
- for (const d of docs.filter((x) => x.tipo === 'roadmap')) {
760
+ for (const d of docs.filter((x) => x.tipo === 'roadmap' && noEscopo(x))) {
719
761
  const novo = substituirBloco(substituirBloco(d.corpo, RELANCE_INI, RELANCE_FIM, renderizarRelance(d)), ESTADO_INI, ESTADO_FIM, renderizarEstado(d));
720
762
  if (novo !== d.corpo) {
721
763
  if (!mudancas.some((m) => m.id === d.id)) {
package/dist/doctor.js CHANGED
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.checarContas = checarContas;
43
+ exports.checarChaveDeEmbedding = checarChaveDeEmbedding;
43
44
  exports.checarOnboarding = checarOnboarding;
44
45
  exports.checar = checar;
45
46
  exports.relatorio = relatorio;
@@ -116,6 +117,37 @@ const ENVS_QUE_REDIRECIONAM = [
116
117
  function versaoNode() {
117
118
  return Number(process.versions.node.split('.')[0]);
118
119
  }
120
+ /**
121
+ * I-38 (T7): a chave de embedding aparece pelo NOME, nunca pelo valor. Ausente e aviso (a busca
122
+ * cai para o fallback local ou para FTS e o regime segue); nome de provider pago e falha, porque a
123
+ * entrada do `ork` apagaria a variavel sob `subscription-only` e o preflight reprovaria o codex.
124
+ */
125
+ function checarChaveDeEmbedding(carregado, env = process.env) {
126
+ const nome = 'chave de embedding';
127
+ const config = (0, manifest_1.configDeEmbedding)(carregado.manifesto);
128
+ if (config.provider === 'none')
129
+ return { nome, nivel: 'ok', detalhe: 'memory.embedding com provider none: busca por significado desligada' };
130
+ const variavel = config.api_key_env;
131
+ if (runtime_ambiente_1.ENVS_DE_PROVIDER_PAGO.includes(variavel)) {
132
+ return { nome, nivel: 'fail', detalhe: `memory.embedding.api_key_env declara ${variavel}, nome da lista de provider pago`,
133
+ correcao: 'use uma chave dedicada ao Orkastery com nome proprio, por exemplo ORKASTERY_EMBEDDING_API_KEY' };
134
+ }
135
+ if (!variavel) {
136
+ return { nome, nivel: 'fail', detalhe: `memory.embedding com provider ${config.provider} sem api_key_env`,
137
+ correcao: 'declare em memory.embedding.api_key_env o NOME da variavel com a chave dedicada' };
138
+ }
139
+ const valor = (env[variavel] ?? '').trim();
140
+ const dsn = carregado.manifesto.memory.database_url_env ? (env[carregado.manifesto.memory.database_url_env] ?? '') : '';
141
+ if (valor !== '' && !(0, orkmind_1.chaveDeEmbeddingAceita)(valor, dsn)) {
142
+ return { nome, nivel: 'fail', detalhe: `${variavel} tem valor recusado: parece URL, DSN ou texto com espaco (valor nunca impresso)`,
143
+ correcao: `confira se ${variavel} guarda a chave dedicada, e nao outra credencial` };
144
+ }
145
+ if (valor !== '') {
146
+ return { nome, nivel: 'ok', detalhe: `${variavel} presente no ambiente (valor nunca impresso); ${config.provider} ${config.model}` };
147
+ }
148
+ return { nome, nivel: 'warn', detalhe: `${variavel} ausente do ambiente: a busca por significado usa o fallback local ou cai para FTS; o regime nao muda`,
149
+ correcao: `exporte ${variavel} com a chave dedicada ao Orkastery (com limite de credito no painel do provider)` };
150
+ }
119
151
  /** Checks locais; ler a entrevista não abre driver, banco, runtime ou rede. */
120
152
  function checarOnboarding(carregado) {
121
153
  const estado = (0, onboarding_1.lerOnboarding)(carregado.raiz);
@@ -315,6 +347,7 @@ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente
315
347
  : 'files (fallback honesto: handoff por arquivos, ponteiro path#ancora)',
316
348
  correcao: estado.pedido === 'orkmind' && estado.efetivo === 'files' ? estado.correcao : undefined,
317
349
  });
350
+ checks.push(checarChaveDeEmbedding(context.loaded));
318
351
  // Bloco B3: a fila de rate limit e estado do mundo, nao detalhe interno. Uma fase
319
352
  // esperando janela e uma fase que NAO esta rodando, e quem olha o doctor precisa
320
353
  // saber disso antes de concluir que a thread travou.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mergeDaEntrega = mergeDaEntrega;
4
4
  exports.registrarEntregaPorPr = registrarEntregaPorPr;
5
+ exports.registrarEntregaExternaPorPr = registrarEntregaExternaPorPr;
5
6
  exports.registrarEntregasPorPr = registrarEntregasPorPr;
6
7
  /**
7
8
  * I-57 (RM-008, fatia 2): a entrega feita por PR tambem prova entrega para o MASTER.
@@ -35,6 +36,18 @@ function mergeDaEntrega(raiz, thread, remoto, base) {
35
36
  }
36
37
  return null;
37
38
  }
39
+ /**
40
+ * A thread chega ao SHIP quando o ciclo dela tem SHIP, e so AVANCA: o segundo PR da mesma entrega (os
41
+ * dois sites da ork-siteshomesco, por exemplo) com a thread ja no MASTER nao a devolve ao SHIP (achado S9).
42
+ */
43
+ function avancarParaShip(raiz, threadId) {
44
+ const atualizada = (0, thread_1.lerThread)(raiz, threadId);
45
+ const fases = atualizada.fases ?? [];
46
+ if (!fases.includes('SHIP') || fases.indexOf(atualizada.faseAtual) >= fases.indexOf('SHIP'))
47
+ return;
48
+ atualizada.faseAtual = 'SHIP';
49
+ (0, thread_1.gravarThread)(raiz, atualizada);
50
+ }
38
51
  function resultado(thread, acao, motivo, extra = {}) {
39
52
  return { thread, acao, motivo, mergeSha: null, headSha: null, ci: null, ...extra };
40
53
  }
@@ -85,15 +98,106 @@ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
85
98
  evidencia: { viaPr: true, ci: { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } },
86
99
  });
87
100
  // Como no `ork ship`: a thread chega ao SHIP quando o ciclo dela tem SHIP.
88
- const atualizada = (0, thread_1.lerThread)(raiz, threadId);
89
- if (atualizada.fases?.includes('SHIP')) {
90
- atualizada.faseAtual = 'SHIP';
91
- (0, thread_1.gravarThread)(raiz, atualizada);
92
- }
101
+ avancarParaShip(raiz, threadId);
93
102
  if (opcoes.publicar !== false)
94
103
  (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
95
104
  return resultado(threadId, 'registrou', `ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR`, { ...merge, ci });
96
105
  }
106
+ const ghApi = (caminho) => (0, util_1.exec)('gh', ['api', caminho, '-H', 'Accept: application/vnd.github+json'], process.cwd(), 120000);
107
+ function lerJson(r) {
108
+ if (!r.ok)
109
+ return null;
110
+ try {
111
+ return JSON.parse(r.stdout);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * `ork ship registrar-pr <thread> --repo <dono/nome> --pr <n>`: a thread que entrega por PR em outro
119
+ * repositorio (os sites, por exemplo) nao tinha como virar `ship_done`, e a entrega virava decisao. A
120
+ * exigencia e a mesma do caminho local: o merge dentro da ponta da base (pela API do GitHub, porque o
121
+ * repositorio nao e remoto deste checkout) e o check declarado verde no head do PR. O repositorio precisa
122
+ * estar declarado no manifesto; e o dono que diz onde, e com que check, a entrega vale. Idempotente.
123
+ */
124
+ function registrarEntregaExternaPorPr(carregado, threadId, opcoes) {
125
+ const raiz = carregado.raiz, repo = opcoes.repositorio, n = opcoes.pr;
126
+ const gh = opcoes.executorGitHub ?? ghApi;
127
+ const declarados = carregado.manifesto.ci.external_repositories;
128
+ if (!Object.hasOwn(declarados, repo)) {
129
+ return resultado(threadId, 'recusada', `o repositorio ${repo} nao esta declarado em ci.external_repositories do orkastery.yaml`);
130
+ }
131
+ if (!Number.isSafeInteger(n) || n < 1)
132
+ return resultado(threadId, 'recusada', `--pr precisa ser o numero do PR, recebido ${String(n)}`);
133
+ const thread = (0, thread_1.lerThread)(raiz, threadId);
134
+ if (thread.status === 'fechada')
135
+ return resultado(threadId, 'ja-registrada', 'thread ja fechada');
136
+ const dir = (0, thread_1.dirThread)(raiz, threadId);
137
+ if ((0, ledger_1.lerLedger)(dir).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.shipConcluido && e.repositorio === repo && e.pr === n)) {
138
+ return resultado(threadId, 'ja-registrada', `o PR #${n} de ${repo} ja tem ship_done`);
139
+ }
140
+ const pr = lerJson(gh(`repos/${repo}/pulls/${n}`));
141
+ if (!pr)
142
+ return resultado(threadId, 'recusada', `a consulta do PR #${n} de ${repo} ao GitHub falhou`);
143
+ const mergeSha = typeof pr.merge_commit_sha === 'string' && /^[0-9a-f]{40}$/.test(pr.merge_commit_sha) ? pr.merge_commit_sha : null;
144
+ const headSha = typeof pr.head?.sha === 'string' && /^[0-9a-f]{40}$/.test(pr.head.sha) ? pr.head.sha : null;
145
+ const base = typeof pr.base?.ref === 'string' && /^[A-Za-z0-9._/-]{1,200}$/.test(pr.base.ref) ? pr.base.ref : null;
146
+ if (pr.merged !== true || !mergeSha || !headSha || !base) {
147
+ return resultado(threadId, 'sem-merge', `o PR #${n} de ${repo} nao esta mesclado`);
148
+ }
149
+ // Achado A4 do CHECK: o PR precisa ser DESTA thread e ter entrado na branch padrao do repositorio. Sem o
150
+ // vinculo, qualquer PR mesclado do repositorio viraria entrega de qualquer thread; sem a base, um PR
151
+ // empilhado numa branch de feature contaria como entrega.
152
+ const vinculo = [pr.title, pr.body, pr.head?.ref].filter((x) => typeof x === 'string')
153
+ // N4 do CHECK 2: a branch do proprio ork e `ork/<thread>-<variante>`, entao o hifen depois do id vale;
154
+ // letra ou digito colado nao (outro id). Sem diferenciar maiusculas.
155
+ .some((x) => new RegExp(`(^|[^a-z0-9-])${threadId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`, 'i').test(x));
156
+ if (!vinculo) {
157
+ return resultado(threadId, 'recusada', `o PR #${n} de ${repo} nao cita a thread ${threadId} no titulo, no corpo nem na branch`, { mergeSha, headSha });
158
+ }
159
+ const repositorio = lerJson(gh(`repos/${repo}`));
160
+ if (!repositorio)
161
+ return resultado(threadId, 'recusada', `a consulta de ${repo} ao GitHub falhou`, { mergeSha, headSha });
162
+ if (repositorio.default_branch !== base) {
163
+ return resultado(threadId, 'recusada', `o PR #${n} entrou em ${base}, e a branch padrao de ${repo} e ${repositorio.default_branch ?? 'desconhecida'}`, { mergeSha, headSha });
164
+ }
165
+ // A ponta da base AGORA, e o merge dentro dela: o push provado, pela mesma pergunta do ls-remote.
166
+ const ramo = lerJson(gh(`repos/${repo}/branches/${encodeURIComponent(base)}`));
167
+ const ponta = typeof ramo?.commit?.sha === 'string' && /^[0-9a-f]{40}$/.test(ramo.commit.sha) ? ramo.commit.sha : '';
168
+ const comparacao = ponta ? lerJson(gh(`repos/${repo}/compare/${mergeSha}...${ponta}`)) : null;
169
+ if (!ponta || !comparacao || !['ahead', 'identical'].includes(String(comparacao.status))) {
170
+ return resultado(threadId, 'recusada', `o merge ${mergeSha.slice(0, 7)} nao esta na ponta de ${repo}:${base}`, { mergeSha, headSha });
171
+ }
172
+ const check = declarados[repo];
173
+ const ci = check ? (0, ci_1.consultarCiDoRepositorio)(repo, headSha, check, opcoes.executorCi) : null;
174
+ if (ci && !ci.ok) {
175
+ return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { mergeSha, headSha, ci });
176
+ }
177
+ (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
178
+ de: thread.base?.branch ?? null,
179
+ para: `${repo}:${base}`,
180
+ repositorio: repo,
181
+ pr: n,
182
+ url: pr.html_url ?? `https://github.com/${repo}/pull/${n}`,
183
+ shaDe: headSha,
184
+ mergeSha,
185
+ jaIncorporado: true,
186
+ remoto: `github:${repo}`,
187
+ shaRemoto: ponta,
188
+ pushVerificado: true,
189
+ fonteDaProva: `gh api repos/${repo}/compare/${mergeSha}...${ponta} (status ${String(comparacao.status)}; ponta de ${base} por gh api repos/${repo}/branches/${base})`,
190
+ autorizadoPor: ci ? 'merge de PR com o CI independente verde no SHA exato' : 'merge de PR em repositorio externo declarado sem CI no manifesto',
191
+ tipoDeAutorizacao: 'pr-externo',
192
+ modo: thread.modo,
193
+ tag: (0, modos_1.tagDoModo)(thread.modo),
194
+ evidencia: { viaPr: true, externo: true, ciExigido: !!ci, ci: ci ? { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } : null },
195
+ });
196
+ avancarParaShip(raiz, threadId);
197
+ if (opcoes.publicar !== false)
198
+ (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
199
+ return resultado(threadId, 'registrou', `ship_done pelo merge ${mergeSha.slice(0, 7)} do PR #${n} de ${repo}`, { mergeSha, headSha, ci });
200
+ }
97
201
  /** `ork ship registrar-pr --todas`: toda thread aberta que ja tem merge `ship(<thread>)` na base. */
98
202
  function registrarEntregasPorPr(carregado, opcoes = {}) {
99
203
  const remoto = opcoes.remoto ?? 'origin';
@@ -0,0 +1,256 @@
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.FIM_EXPERIENCIA = exports.INICIO_EXPERIENCIA = void 0;
37
+ exports.caminhoSeguro = caminhoSeguro;
38
+ exports.planejarExperiencia = planejarExperiencia;
39
+ exports.aplicarExperiencia = aplicarExperiencia;
40
+ /** Bloco próprio nos arquivos lidos pelo host. Nunca altera o bloco de ork init. */
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const node_crypto_1 = require("node:crypto");
44
+ exports.INICIO_EXPERIENCIA = '<!-- orkastery:experiencia:begin -->';
45
+ exports.FIM_EXPERIENCIA = '<!-- orkastery:experiencia:end -->';
46
+ const CONTRATO = 'ork.experiencia-install/v1';
47
+ const LINHA_DO_DIRETORIO = /^Se ativo, leia a skill selecionada pelo campo skill no diretório (".*")\.$/;
48
+ const conflito = () => { throw Error('experiencia.bloco.conflict: bloco ou recibo alterado/duplicado; preserve os arquivos e revise a instalação'); };
49
+ /** Recusa symlinks e hardlinks em qualquer componente antes da leitura/escrita. */
50
+ function caminhoSeguro(projeto, arquivo) {
51
+ if (fs.realpathSync(projeto) !== projeto || !arquivo.startsWith(projeto + path.sep))
52
+ throw Error('experiencia.path.unsafe');
53
+ let atual = projeto;
54
+ const partes = path.relative(projeto, arquivo).split(path.sep);
55
+ for (let i = 0; i < partes.length; i++) {
56
+ atual = path.join(atual, partes[i]);
57
+ const s = fs.lstatSync(atual, { throwIfNoEntry: false });
58
+ if (!s)
59
+ continue;
60
+ if (s.isSymbolicLink() || (i < partes.length - 1 ? !s.isDirectory() : !s.isFile() || s.nlink !== 1))
61
+ throw Error('experiencia.path.unsafe');
62
+ }
63
+ }
64
+ function ler(projeto, arquivo) {
65
+ caminhoSeguro(projeto, arquivo);
66
+ return fs.existsSync(arquivo) ? fs.readFileSync(arquivo) : null;
67
+ }
68
+ function iguais(a, b) { return a === null ? b === null : b !== null && a.equals(b); }
69
+ function contar(b, texto) {
70
+ let n = 0, i = 0;
71
+ while ((i = b.indexOf(texto, i)) >= 0) {
72
+ n++;
73
+ i += Buffer.byteLength(texto);
74
+ }
75
+ return n;
76
+ }
77
+ function decodificar(s) {
78
+ if (typeof s !== 'string')
79
+ return conflito();
80
+ const b = Buffer.from(s, 'base64');
81
+ if (b.toString('base64') !== s)
82
+ return conflito();
83
+ return b;
84
+ }
85
+ 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.',
93
+ exports.FIM_EXPERIENCIA, ''].join(eol));
94
+ }
95
+ /**
96
+ * 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.
98
+ */
99
+ function reconhecerBloco(antes) {
100
+ if (contar(antes, exports.INICIO_EXPERIENCIA) !== 1 || contar(antes, exports.FIM_EXPERIENCIA) !== 1)
101
+ return null;
102
+ const inicio = antes.indexOf(exports.INICIO_EXPERIENCIA), marcaFim = antes.indexOf(exports.FIM_EXPERIENCIA);
103
+ if (marcaFim < inicio || (inicio > 0 && antes[inicio - 1] !== 10))
104
+ return null;
105
+ let fim = marcaFim + Buffer.byteLength(exports.FIM_EXPERIENCIA);
106
+ const eol = eolDe(antes.subarray(inicio, fim));
107
+ if (antes.subarray(fim, fim + eol.length).toString() === eol)
108
+ fim += eol.length;
109
+ const texto = antes.subarray(inicio, fim);
110
+ 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)))
123
+ return null;
124
+ // Uma linha em branco antes do bloco é o separador que a instalação põe depois de uma linha.
125
+ const sep = Buffer.from(eol);
126
+ const separador = inicio >= 2 * sep.length && antes.subarray(inicio - sep.length, inicio).equals(sep) &&
127
+ antes[inicio - sep.length - 1] === 10 ? sep : Buffer.alloc(0);
128
+ return { inicio, fim, separador, eol, doRecibo: false };
129
+ }
130
+ /**
131
+ * Remove o bloco e, se ainda intacto, o separador. O separador de uma quebra só sai quando a linha
132
+ * anterior continua terminada; o de duas, quando a última linha do conteúdo original continua sem
133
+ * quebra. Conteúdo externo depois do bloco não se funde à última linha do prefixo.
134
+ */
135
+ function removerBloco(antes, bloco) {
136
+ let inicio = bloco.inicio;
137
+ const sep = bloco.separador, antesDoSep = bloco.inicio - sep.length;
138
+ if (sep.length && antesDoSep > 0 && antes.subarray(antesDoSep, bloco.inicio).equals(sep)) {
139
+ const terminaEmLinha = antes[antesDoSep - 1] === 10;
140
+ if ((contar(sep, '\n') === 1) === terminaEmLinha)
141
+ inicio = antesDoSep;
142
+ }
143
+ const prefixo = antes.subarray(0, inicio), sufixo = antes.subarray(bloco.fim);
144
+ const juncao = prefixo.length && prefixo.at(-1) !== 10 && sufixo.length ? Buffer.from(bloco.eol) : Buffer.alloc(0);
145
+ return Buffer.concat([prefixo, juncao, sufixo]);
146
+ }
147
+ /** Planejamento puro em relação ao disco: dry-run e preflight usam o mesmo contrato. */
148
+ function planejarExperiencia(projeto, host, skill) {
149
+ projeto = fs.realpathSync(path.resolve(projeto));
150
+ if (!['claude-code', 'codex'].includes(host))
151
+ throw Error('experiencia.host.unsupported');
152
+ if (skill !== null && /[\r\n`]/.test(skill))
153
+ throw Error('experiencia.skill.invalid');
154
+ const arquivo = path.join(projeto, host === 'codex' ? 'AGENTS.md' : 'CLAUDE.md');
155
+ const reciboPath = path.join(projeto, '.orkastery', 'experiencia', host + '.json');
156
+ const antes = ler(projeto, arquivo), bytesRecibo = ler(projeto, reciboPath);
157
+ let recibo = null;
158
+ if (bytesRecibo) {
159
+ try {
160
+ recibo = JSON.parse(bytesRecibo.toString('utf8'));
161
+ }
162
+ catch {
163
+ conflito();
164
+ }
165
+ if (!recibo || recibo.contrato !== CONTRATO || recibo.host !== host)
166
+ return conflito();
167
+ if (recibo.anterior !== null)
168
+ decodificar(recibo.anterior);
169
+ }
170
+ const marcas = antes ? contar(antes, exports.INICIO_EXPERIENCIA) + contar(antes, exports.FIM_EXPERIENCIA) : 0;
171
+ // Sem marcas, um recibo que sobrou (arquivo apagado ou bloco tirado à mão) não descreve nada.
172
+ let bloco = null;
173
+ if (antes && marcas) {
174
+ if (recibo) {
175
+ const adicao = decodificar(recibo.adicao), corte = adicao.indexOf(exports.INICIO_EXPERIENCIA);
176
+ if (contar(adicao, exports.INICIO_EXPERIENCIA) !== 1 || contar(adicao, exports.FIM_EXPERIENCIA) !== 1 || corte < 0)
177
+ return conflito();
178
+ const nucleo = adicao.subarray(corte), inicio = antes.indexOf(nucleo);
179
+ if (marcas === 2 && inicio >= 0) {
180
+ bloco = { inicio, fim: inicio + nucleo.length, separador: adicao.subarray(0, corte), eol: eolDe(nucleo), doRecibo: true };
181
+ }
182
+ }
183
+ bloco ??= reconhecerBloco(antes);
184
+ if (!bloco)
185
+ return conflito();
186
+ }
187
+ let depois = antes, proximoRecibo = null;
188
+ if (skill !== null) {
189
+ let separador, nucleo, anterior;
190
+ if (antes && bloco) {
191
+ // Atualização no lugar: o que está antes e depois do bloco, inclusive edição externa, fica.
192
+ nucleo = nucleoDoBloco(skill, bloco.eol);
193
+ separador = bloco.separador;
194
+ depois = Buffer.concat([antes.subarray(0, bloco.inicio), nucleo, antes.subarray(bloco.fim)]);
195
+ // Adotado sem recibo: se só havia o bloco, o arquivo conta como criado pelo pacote.
196
+ const semBloco = removerBloco(antes, bloco);
197
+ anterior = bloco.doRecibo ? recibo.anterior : semBloco.length ? semBloco.toString('base64') : null;
198
+ }
199
+ else {
200
+ const base = antes ?? Buffer.alloc(0), eol = eolDe(base);
201
+ nucleo = nucleoDoBloco(skill, eol);
202
+ separador = Buffer.from(base.length ? (base.at(-1) === 10 ? eol : eol + eol) : '');
203
+ depois = Buffer.concat([base, separador, nucleo]);
204
+ anterior = antes?.toString('base64') ?? null;
205
+ }
206
+ proximoRecibo = Buffer.from(JSON.stringify({ contrato: CONTRATO, host, anterior,
207
+ adicao: Buffer.concat([separador, nucleo]).toString('base64'),
208
+ }, null, 2) + '\n');
209
+ }
210
+ else if (antes && bloco) {
211
+ depois = removerBloco(antes, bloco);
212
+ // Vazio só some quando o arquivo não existia (recibo) ou quando, sem recibo, só havia o bloco.
213
+ if (depois.length === 0 && (recibo ? recibo.anterior === null : true))
214
+ depois = null;
215
+ }
216
+ const mudancas = [{ arquivo, antes, depois }, { arquivo: reciboPath, antes: bytesRecibo, depois: proximoRecibo }]
217
+ .filter(m => !iguais(m.antes, m.depois));
218
+ return { projeto, host, acao: skill === null ? 'remover' : 'instalar', mudancas };
219
+ }
220
+ /** Revalida o plano inteiro; falha de gravação restaura as mudanças já aplicadas. */
221
+ function aplicarExperiencia(plano) {
222
+ for (const m of plano.mudancas)
223
+ if (!iguais(ler(plano.projeto, m.arquivo), m.antes))
224
+ throw Error('experiencia.plan.stale');
225
+ const aplicadas = [];
226
+ const gravar = (arquivo, conteudo) => {
227
+ caminhoSeguro(plano.projeto, arquivo);
228
+ if (conteudo === null) {
229
+ fs.unlinkSync(arquivo);
230
+ return;
231
+ }
232
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
233
+ const tmp = arquivo + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
234
+ try {
235
+ // Arquivo novo segue o umask, como um CLAUDE.md criado à mão.
236
+ const modo = fs.statSync(arquivo, { throwIfNoEntry: false })?.mode ?? 0o666;
237
+ fs.writeFileSync(tmp, conteudo, { flag: 'wx', mode: modo });
238
+ fs.renameSync(tmp, arquivo);
239
+ }
240
+ finally {
241
+ if (fs.existsSync(tmp))
242
+ fs.unlinkSync(tmp);
243
+ }
244
+ };
245
+ try {
246
+ for (const m of plano.mudancas) {
247
+ gravar(m.arquivo, m.depois);
248
+ aplicadas.push(m);
249
+ }
250
+ }
251
+ catch (e) {
252
+ for (const m of aplicadas.reverse())
253
+ gravar(m.arquivo, m.antes);
254
+ throw e;
255
+ }
256
+ }