@aksp/opencrew 1.7.1 → 1.8.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,66 @@
3
3
  All notable changes to opencrew are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## [1.8.0] — 2026-10-07
7
+
8
+ Fase U3a, fatia 1 "Pasta de entrega" (`specs/fase-u3a1-pasta-de-entrega.md`). Chega a quem já usa
9
+ com um `npx @aksp/opencrew@latest update`, e funciona nas crews que já existem, sem mexer nelas.
10
+
11
+ Ainda não nesta versão: PDF e "posts formatados" não são gerados pela entrega (`artigo.md`,
12
+ `corpo.md` e os roteiros saem em markdown, e o LEIA-ME ensina a salvar como PDF pelo "Imprimir");
13
+ copiar a entrega para uma pasta do projeto, registrar "entregar assim mesmo" e marcar "já
14
+ publicado" ficam para a 1.9.0. O LEIA-ME e os nomes dos arquivos são só em português.
15
+
16
+ ### Added
17
+ - **Pasta `entrega/`: o que você usa, separado por canal.** Depois da aprovação final, a crew
18
+ monta `crews/<crew>/output/<execução>/entrega/` com uma pasta por canal (`instagram/`,
19
+ `linkedin/`, `blog/`, `email/`, `whatsapp/`, `twitter/`, `youtube/`; só os que a execução tem).
20
+ Antes, você recebia a pasta da execução com `v1`, `v2`, relatórios e textos cheios de `#`, `**`
21
+ e rótulos.
22
+ - **Texto pronto para colar.** `legenda.txt`, `post.txt` e `tweet.txt` saem sem `#`, `**`, rótulos
23
+ nem recados internos, com as hashtags no fim. O primeiro comentário do LinkedIn vem em arquivo
24
+ à parte; a thread, em `tweet-1.txt`, `tweet-2.txt`…; o blog, em `seo.txt` (título, meta
25
+ description, palavra-chave, slug) e `artigo.md`; o e-mail, em `assunto.txt`, `previa.txt` e
26
+ `corpo.md`; o WhatsApp, em `mensagem.txt`. As imagens vão para a pasta do canal com o nome
27
+ original, sem alteração; o HTML dos slides, para `editaveis/`.
28
+ - **`LEIA-ME.md` em cada entrega.** Diz o que fazer com cada arquivo, canal por canal, em passos
29
+ numerados; marca cada canal como "Pronto" ou "Não está pronto"; lista em "Antes de usar" o que
30
+ falta; e diz o que não foi conferido (links e fatos, texto dentro das imagens, aparência final
31
+ em cada rede).
32
+ - **O que não está pronto fica marcado.** Texto com `[PREENCHER]`, acima de um limite ou arquivo
33
+ que faltou: o canal aparece como "Não está pronto" e a crew pergunta se você quer corrigir agora
34
+ ou seguir assim. Se a legenda, o post ou o tweet passar do limite por causa das hashtags no fim,
35
+ o LEIA-ME traz um alerta.
36
+ - **Arquivo sem canal não se perde.** Proposta, minuta, relatório ou formato que não é de rede
37
+ nenhuma vai inteiro para `outros/`, com o nome original, e aparece no LEIA-ME.
38
+ - **Entrega de uma execução antiga.** Peça à IA para montar a entrega de uma execução já
39
+ encerrada: ela lista os arquivos, pede o seu "sim" e monta a pasta.
40
+ - Crew que publica sozinha (Instagram, por exemplo): a entrega é montada antes da publicação, e
41
+ o LEIA-ME avisa "Esta crew publica este canal sozinha. Antes de postar à mão, confira se já saiu."
42
+
43
+ ### Changed
44
+ - **O fim da execução aponta para `entrega/` e para o LEIA-ME**, não mais para a pasta da execução
45
+ e para um "arquivo final" que ninguém dizia qual era. A cópia do arquivo final na raiz da
46
+ execução deixa de ser feita.
47
+ - A pasta `entrega/` é refeita do zero a cada entrega e fica fora do git: o que você editar ali se
48
+ perde. Para guardar, copie a pasta para outro lugar do projeto (o LEIA-ME avisa).
49
+ - Se o script da entrega não rodar (sem Node, por exemplo), a execução não para: a crew avisa e
50
+ lista os arquivos aprovados.
51
+
52
+ ### Fixed
53
+ - **O título do arquivo era lido como legenda.** Num arquivo com `# Legenda — …` no topo e a
54
+ legenda de verdade mais abaixo, o verificador media duas legendas e podia dar alerta falso.
55
+ Agora o título do arquivo não conta como peça.
56
+
57
+ ### Internal
58
+ - `_opencrew/core/scripts/entregar.mjs` e os módulos de `scripts/entrega/` (sem dependência, até
59
+ 200 linhas cada); `_opencrew/core/prompts/entrega.prompt.md`; seção `### Entrega` no runner, que
60
+ perde "Save final output", "Run folder" e "Output saved to".
61
+ - Regra 15 do `AGENTS.md`: script do runtime só escreve onde foi combinado.
62
+ - Travas novas: `tests/entregar*.test.js`, `tests/runtime-contracts-u3a.test.js`,
63
+ `tests/upgrade-u3a.test.js` e o U3a-14c em `tests/package.test.js`.
64
+ - Roteiro renumerado: U3a fatia 2 = 1.9.0, U3b = 1.10.0, U4 = 1.11.0.
65
+
6
66
  ## [1.7.1] — 2026-10-06
7
67
 
8
68
  Fase R3 "Reparos do runner em uso real" (`specs/fase-r3-runner-em-uso-real.md`): dois defeitos
package/README.md CHANGED
@@ -32,6 +32,9 @@ dentro da sua IDE.**
32
32
  mensal, lançamento de produto. Comece em 2 minutos.
33
33
  - 📤 **Exportação multi-formato** — PDF, CSV e posts formatados por plataforma,
34
34
  sem abrir editor nenhum.
35
+ - 📬 **Entrega por canal** — depois de aprovar, você encontra a pasta `entrega/`: uma pasta por
36
+ canal (Instagram, LinkedIn, blog, e-mail, WhatsApp, X/Twitter, YouTube), o texto pronto para
37
+ colar, as imagens e um `LEIA-ME.md` com o passo a passo. O que não está pronto fica marcado.
35
38
  - 🎛️ **Seleção inteligente de agentes** — o sistema analisa seu pedido e
36
39
  sugere quais agentes são necessários para aquela tarefa. Você confirma ou
37
40
  ajusta com um clique. Agentes pulados não gastam tokens naquele run.
@@ -178,9 +181,9 @@ meu-projeto/
178
181
  │ │ ├── skills.engine.md ← gerenciador de skills
179
182
  │ │ ├── architect.agent.yaml ← definição do Arquiteto
180
183
  │ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
181
- │ │ ├── scripts/ ← verificador, conferência de fontes e os scripts do Escritório
184
+ │ │ ├── scripts/ ← verificador, conferência de fontes, caminhos, entrega e os scripts do Escritório
182
185
  │ │ ├── escritorio/ ← página do Escritório ao vivo (abre com /opencrew dashboard)
183
- │ │ └── prompts/ ← 13 prompts de fase (discovery, design, build, etc.)
186
+ │ │ └── prompts/ ← 14 prompts de fase (discovery, design, build, entrega, etc.)
184
187
  │ ├── agents/ ← 5 agentes base compartilhados
185
188
  │ │ ├── researcher.agent.md
186
189
  │ │ ├── copywriter.agent.md
@@ -194,6 +197,9 @@ meu-projeto/
194
197
  │
195
198
  ├── crews/ ← suas crews vivem aqui
196
199
  │ ├── blog-semanal/ ← template: blog semanal
200
+ │ │ └── output/<execução>/ ← criada a cada execução
201
+ │ │ ├── v1/ v2/ … ← o que cada passo gravou
202
+ │ │ └── entrega/ ← o que você usa: LEIA-ME.md + uma pasta por canal
197
203
  │ ├── instagram-carrossel/ ← template: Instagram carrossel
198
204
  │ ├── newsletter-mensal/ ← template: newsletter
199
205
  │ └── lancamento-produto/ ← template: lançamento
@@ -209,6 +215,50 @@ meu-projeto/
209
215
 
210
216
  ---
211
217
 
218
+ ## Entrega por canal
219
+
220
+ Depois da aprovação final, a crew monta a pasta `entrega/` dentro da pasta da execução
221
+ (`crews/<crew>/output/<execução>/entrega/`). É ali que está o que você vai usar:
222
+
223
+ ```
224
+ entrega/
225
+ ├── LEIA-ME.md ← comece por aqui: o que fazer com cada arquivo, canal por canal
226
+ ├── instagram/ ← legenda.txt (hashtags no fim) e as imagens, com o nome original
227
+ ├── linkedin/ ← post.txt e, se houver, post-comentario.txt (o primeiro comentário)
228
+ ├── blog/ ← seo.txt (título, meta description, palavra-chave, slug) e artigo.md
229
+ ├── email/ ← assunto.txt, previa.txt e corpo.md
230
+ ├── whatsapp/ ← mensagem.txt
231
+ ├── twitter/ ← tweet.txt (na thread: tweet-1.txt, tweet-2.txt…)
232
+ ├── youtube/ ← o roteiro
233
+ ├── outros/ ← arquivo sem canal (proposta, minuta, relatório), como está
234
+ └── editaveis/ ← o HTML dos slides, para quem quiser ajustar
235
+ ```
236
+
237
+ Só aparecem as pastas que a execução tem.
238
+
239
+ - **Texto pronto para colar.** Os `.txt` saem sem `#`, `**`, rótulos nem recados internos, com as
240
+ hashtags no fim. Com mais de uma peça do mesmo tipo, os arquivos são numerados (`post-1.txt`,
241
+ `post-2.txt`).
242
+ - **O `LEIA-ME.md` diz o que fazer.** Cada canal tem a situação ("Pronto" ou "Não está pronto"),
243
+ os arquivos, de onde cada um veio e os passos, numerados. "Antes de usar" junta o que falta;
244
+ "O que não foi conferido" lembra o que ninguém mediu (links e fatos, texto dentro das imagens,
245
+ aparência final em cada rede).
246
+ - **O que não está pronto fica marcado.** Sobrou um `[PREENCHER]` ou um texto acima do limite? O
247
+ canal aparece como "Não está pronto" e a crew pergunta se você quer corrigir agora ou seguir
248
+ assim.
249
+ - **Arquivo sem canal vai para `outros/`**, inteiro e com o nome original: nada some.
250
+ - **A pasta é refeita a cada entrega e fica fora do git.** O que você editar ali se perde; para
251
+ guardar, copie a pasta para outro lugar do projeto.
252
+ - **Execução antiga?** Peça à IA: "monte a entrega da execução X da crew Y". Ela lista os
253
+ arquivos, pede o seu "sim" e monta a pasta. Funciona em crews criadas antes da 1.8.0, depois
254
+ do `update`.
255
+
256
+ A entrega não gera PDF nem imagem: `artigo.md`, `corpo.md` e os roteiros saem em markdown, e o
257
+ LEIA-ME ensina a salvar como PDF pelo "Imprimir" do seu editor. O LEIA-ME e os nomes dos arquivos
258
+ são sempre em português.
259
+
260
+ ---
261
+
212
262
  ## Escritório ao vivo
213
263
 
214
264
  Quer ver a equipe trabalhando? O **Escritório** é uma página em pixel-art, aberta no navegador,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aksp/opencrew",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -61,6 +61,7 @@ Route input to the matching action:
61
61
  | `/opencrew dashboard` | Turn on and open the Escritório (live view) — see "Dashboard (Optional)" |
62
62
  | `/opencrew dashboard off` | Turn the Escritório off — see "Dashboard (Optional)" |
63
63
  | `/opencrew reset` | Confirm and reset all configuration |
64
+ | Request to deliver a run that already ended ("monte a entrega da execução …") | Load `_opencrew/core/prompts/entrega.prompt.md` → build the `entrega/` folder of that run |
64
65
  | Natural language about crews | Infer intent and route accordingly |
65
66
 
66
67
  ## Loading Agents
@@ -120,6 +121,8 @@ and touch nothing else: stop no process, delete no file.
120
121
  - Exception: crew memory scaffolding (`memories.md` headers, `runs.md` columns)
121
122
  keeps fixed PT-BR structural labels regardless of the user's language —
122
123
  see `_opencrew/core/runner.pipeline.md`
124
+ - Exception: the delivery folder (`entrega/`) — its folder names, file names and the `LEIA-ME.md`
125
+ are written by a script in fixed PT-BR, whatever the user's language
123
126
 
124
127
  ## Critical Rules
125
128
 
@@ -1 +1 @@
1
- 1.7.1
1
+ 1.8.0
@@ -0,0 +1,131 @@
1
+ # Entrega — The Delivery Folder of a Run
2
+
3
+ One script turns the approved files of a run into `crews/{name}/output/{run_id}/entrega/`: one
4
+ folder per channel, text ready to paste and a `LEIA-ME.md` that tells the user what to do with each
5
+ file. Your part is to build the list of files, run the script and act on its last line.
6
+
7
+ You do NOT copy, split, rename or rewrite a file yourself, and you never write inside `entrega/`:
8
+ the script rebuilds that folder from scratch on every call. **When** the delivery runs is in the
9
+ section "Entrega" of `_opencrew/core/runner.pipeline.md`; a request to deliver a run that is
10
+ already over starts at "A run that already ended", below.
11
+
12
+ ## Step 1: Build the list
13
+
14
+ `{lista}` is one item per file, separated by commas, each written `{caminho}={formato}`.
15
+
16
+ - **What goes in:** for each **creation or rendering step** of the approved run, every path the
17
+ `saida` action of `caminho.mjs` returned the **last time** the step ran (a step sent back by the
18
+ reviewer ran more than once: only its last version counts) — all the output files of the step,
19
+ images and HTML included. Use the paths you stored during the run. Never look for a `vN` folder
20
+ by yourself and never assume `v1`.
21
+ - **`{formato}`** is the `format:` of the step that wrote the file. A rendering step with no
22
+ `format:` uses the `format:` of the content step it renders (the slides of an `instagram-feed`
23
+ carousel go as `=instagram-feed`). With no format either way, with an export format (`pdf`,
24
+ `csv`, `formatted-post`) or with one outside `[a-z0-9-]+`, the item goes without `={formato}`:
25
+ the script puts that file in `outros/`.
26
+ This list is **not** the list of the checker (`verificar.mjs`), where a step with no `format:`
27
+ goes without one: here the rendering step (images, HTML) takes the format of the content step it
28
+ renders — the step that wrote its `inputFile`. Without it the images land in `outros/`.
29
+ - **Stay out:** research, briefing, the reviewer's verdict, checkpoint answers and every skipped
30
+ step (a deselected agent, or a step the user skipped).
31
+ - **Safe names** — the safe-name rule (nome seguro) of the runner applies. A file that rule left
32
+ out of commands stays out of the list too; tell the user:
33
+ `{arquivo} — ficou fora da entrega: nome com caractere que não vai em comando`.
34
+
35
+ ## Step 2: Channels the crew publishes by itself
36
+
37
+ Add `--vai-publicar {canal}` once for each channel of the list that has an irreversible step in the
38
+ pipeline (`side_effects: irreversible`, in the step or in the agent's skill), whether that step
39
+ already ran or not. The `LEIA-ME.md` then opens that channel with: "Esta crew publica este canal
40
+ sozinha. Antes de postar à mão, confira se já saiu."
41
+
42
+ - The channel of a step is the `platform:` of its `format:` — read it in
43
+ `_opencrew/best-practices.local/{format}.md` when that file declares `platform:`, otherwise in
44
+ `_opencrew/core/best-practices/{format}.md`. For a step with no `format:`, the one of the skill
45
+ (`instagram-publisher` → `instagram`). With neither, do not pass the option.
46
+ - `{canal}` is the folder name: `instagram`, `linkedin`, `blog`, `email`, `whatsapp`, `twitter` or
47
+ `youtube`. Only a channel that has an item in the list (an item whose format has that
48
+ `platform:`) — for any other the script stops with `Canal não encontrado nesta entrega: {canal}.`
49
+
50
+ ## Step 3: Run the script
51
+
52
+ From the project root, one line, everything between double quotes:
53
+
54
+ `node _opencrew/core/scripts/entregar.mjs --crew "crews/{name}" --run "{run_id}" --arquivo "{lista}"`
55
+
56
+ With channels from Step 2, the same command ends with one option per channel:
57
+
58
+ `node _opencrew/core/scripts/entregar.mjs --crew "crews/{name}" --run "{run_id}" --arquivo "{lista}" --vai-publicar {canal}`
59
+
60
+ ## Step 4: Show the result and read the last line
61
+
62
+ The output of the script is the final summary of the run (the folder, each channel as "Pronto" or
63
+ "Não está pronto", what is missing, the path of the `LEIA-ME.md`): show it to the user as it came,
64
+ without the `ENTREGA:` line. Do not rewrite it and do not add files it does not list. Besides the
65
+ `entrega/` folder, the script also writes `crews/{name}/output/{run_id}/verificacao-entrega.md`,
66
+ the report of the check made at delivery time (it is not part of the delivery).
67
+
68
+ - `ENTREGA:OK` → go on with the run.
69
+ - `ENTREGA:INCOMPLETA` → a channel is not ready, or a file could not be written. The files were
70
+ generated anyway and the `LEIA-ME.md` marks the channel. Show what is missing and ask:
71
+ ```
72
+ ⚠️ A entrega ficou incompleta: {o que falta}
73
+ 1. Corrigir agora (eu ajusto e monto a entrega de novo)
74
+ 2. Seguir assim (no LEIA-ME, o canal fica marcado como "Não está pronto")
75
+ ```
76
+ Wait for the answer.
77
+ - **1** — for each item that is missing, fix it in the source file the summary names: ask the
78
+ user for the real information of every `[PREENCHER: …]`, shorten what is over a limit, write
79
+ again a file that is not there (when the summary says a file could not be written, there is
80
+ nothing to fix in the text). Then run the delivery again, with the same list.
81
+ - **2** — go on, with the delivery as it is. Every irreversible step still asks for its own
82
+ confirmation, as it does today.
83
+
84
+ After "Edit this content" (the final menu of the runner) changes an approved file, or any step
85
+ runs again after the delivery, run the delivery again with the new paths: the folder is rebuilt
86
+ from scratch, so whatever was edited inside `entrega/` is lost — the `LEIA-ME.md` says so.
87
+
88
+ ## When the script does not run
89
+
90
+ The script did not run when there is no Node, an error, or no `ENTREGA:` line at the end (it prints
91
+ one line in PT-BR and stops, with nothing written). Then never build the folder by hand. If the line points to something in the
92
+ command you wrote (an option, a channel, the run), fix the command and run it once more. Otherwise
93
+ tell the user, list the approved files (the paths of your list) and go on with the run:
94
+
95
+ ```
96
+ ⚠️ A entrega automática não rodou: {motivo}
97
+ Os arquivos aprovados estão em:
98
+ - {caminho}
99
+ ```
100
+
101
+ `{motivo}` is the line the script printed, or what kept it from running. The completion summary
102
+ of the runner then shows this list in place of the `entrega/` folder.
103
+
104
+ ## A run that already ended
105
+
106
+ When the user asks to deliver a run that is over (it was run before this folder existed, or the
107
+ delivery did not run):
108
+
109
+ 1. Find the crew and the run. If the user did not say which, list the folders of
110
+ `crews/{name}/output/` with the IDE's folder-listing tool (no shell command) and ask.
111
+ 2. Read the steps of `crews/{name}/pipeline/`. For the `outputFile` of each creation or rendering
112
+ step (same rules of Step 1 for what stays out and for `{formato}`), run:
113
+ `node _opencrew/core/scripts/caminho.mjs "{name}" entrada --run "{run_id}" --arquivo "{outputFile}"`
114
+ `CAMINHO:OK {path}` → that path is the item. `CAMINHO:FALTA` → the file is not in that run:
115
+ leave it out.
116
+ 3. Nobody recorded what was approved in that run, so show the list and wait for the "sim":
117
+ ```
118
+ Entrega da execução {run_id} da crew {name}. Arquivos:
119
+ - {caminho} ({formato})
120
+ Posso montar a entrega com esta lista? (sim / não)
121
+ ```
122
+ 4. On "sim", follow Steps 2 to 4. No step of the pipeline runs again, and nothing is published.
123
+
124
+ ## Rules
125
+
126
+ - **DO** show the output of the script as it came; the folder and file names and the `LEIA-ME.md`
127
+ are fixed PT-BR, whatever the user's language.
128
+ - **DO** run the delivery again whenever an approved file changes.
129
+ - **DO NOT** create, edit or delete anything inside `entrega/` yourself.
130
+ - **DO NOT** put in the list a file the user did not approve, nor a path you guessed.
131
+ - **DO NOT** treat `ENTREGA:INCOMPLETA` as an error of the script: it is its answer.
@@ -500,6 +500,7 @@ root the one-line command of each moment and read the last line (`CAMINHO:OK {pa
500
500
  is empty → this check never fires (legacy behavior).
501
501
 
502
502
  0b. **Escritório** — if it is on, run `passo`, or `checkpoint` when the step is a checkpoint (see "Escritório" above).
503
+ 0c. **Entrega** — before the first step that publishes or sends, once the final approval was given, run the delivery (see "Entrega" below).
503
504
 
504
505
  1. **Pre-Step Input Validation** — MANDATORY. If the step's frontmatter declares an `inputFile`, the input comes from the `entrada` action, never from a path you build: validate that the input exists before executing the step. Run the `entrada` command (Output Path Transformation) with the `inputFile` as declared:
505
506
  - `CAMINHO:OK {path}` → that path is the step's input (the newest version that has the file): read the input from it and execute the step.
@@ -715,6 +716,7 @@ For reference, the complete execution order for each pipeline step is:
715
716
  ```
716
717
  0. Agent deselection check (skip step if its agent was deselected)
717
718
  0b. Escritório command (passo or checkpoint) — only if it is on
719
+ 0c. Entrega (delivery script) — only before the first step that publishes or sends
718
720
  1. Pre-Step Input Validation (script gate: `entrada`)
719
721
  2. Read step file
720
722
  3. Check execution mode and execute (subagent / inline / checkpoint)
@@ -724,10 +726,18 @@ For reference, the complete execution order for each pipeline step is:
724
726
 
725
727
  Steps 1 and 4 are binary script gates. If either fails, the pipeline does NOT advance — the user is consulted.
726
728
 
729
+ ### Entrega
730
+
731
+ One script turns the approved files into `crews/{name}/output/{run_id}/entrega/` (a folder per channel, text ready to paste, a `LEIA-ME.md`). Read `_opencrew/core/prompts/entrega.prompt.md` and follow it: how to build `{lista}`, when to add `--vai-publicar`, what to do with `ENTREGA:OK`, with `ENTREGA:INCOMPLETA` and with a script that did not run.
732
+
733
+ - **Command** — from the project root, by the safe-name rule (nome seguro), everything between double quotes: `node _opencrew/core/scripts/entregar.mjs --crew "crews/{name}" --run "{run_id}" --arquivo "{lista}"`
734
+ - **When** — once, after the final approval, immediately before the first step that publishes or sends (`side_effects: irreversible`, in the step or in the agent's skill); with no such step, after the last step. Always before the end-of-run command of the Escritório. If the irreversible step comes before the final approval (a crew built by an old version), the delivery runs at the end.
735
+ - **Crew with no final approval checkpoint** — same moments, and show `Esta crew não tem aprovação final: confira os arquivos antes de usar.` **Never** for a run that was rejected, aborted before the final approval or left with no approved file.
736
+ - The output of the script is the final summary of the run: show it as it came; if the run stops later, at an irreversible step, show it before stopping. After "Edit this content" changes an approved file, run it again.
737
+
727
738
  ### After Pipeline Completion
728
739
 
729
- 1. Save final output to `crews/{name}/output/{run_id}/{filename}.md`
730
- (The run folder was created during initialization — no separate date subfolder needed)
740
+ 1. **Entrega** — if the delivery has not run in this run, run it now (see "Entrega" above).
731
741
  1b. **Escritório** — if it is on, run `concluir` (see "Escritório" above).
732
742
 
733
743
  2. **Update crew memory** — write to BOTH files:
@@ -828,8 +838,7 @@ Steps 1 and 4 are binary script gates. If either fails, the pipeline does NOT ad
828
838
  ```
829
839
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
830
840
  ✅ Pipeline complete!
831
- 📁 Run folder: crews/{name}/output/{run_id}/
832
- 📄 Output saved to: {output path}
841
+ 📁 Delivery: crews/{name}/output/{run_id}/entrega/ — start with LEIA-ME.md
833
842
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
834
843
 
835
844
  What would you like to do?
@@ -0,0 +1,46 @@
1
+ // Linha de comando do `entregar.mjs`: as opções, a lista `caminho=formato` e a linha de uso.
2
+ // Spec: fase-u3a1-pasta-de-entrega.md, §3 e §6 (repositório do OpenCrew).
3
+ import { lerItemDaLista } from '../verificar/argumentos.mjs';
4
+
5
+ export const USO = 'Uso: node _opencrew/core/scripts/entregar.mjs --crew "crews/<crew>" --run "<id>" --arquivo "<caminho=formato>[,<caminho=formato>…]" [--vai-publicar <canal>] [--ajuda]';
6
+
7
+ const OPCAO = /^--(crew|run|arquivo|vai-publicar)(?:=(.*))?$/s;
8
+
9
+ /** Texto que veio da linha de comando e volta numa mensagem: uma linha só, até 200 caracteres. */
10
+ export const limpar = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice(0, 200);
11
+
12
+ function guardar(args, nome, valor) {
13
+ if (nome === 'arquivo') args.arquivos.push(valor);
14
+ else if (nome === 'vai-publicar') args.vaiPublicar.push(valor.trim().toLowerCase());
15
+ else args[nome] = valor;
16
+ }
17
+
18
+ /**
19
+ * `argv` → `{ crew, run, arquivos, vaiPublicar, ajuda, desconhecida }`. Opção vale como
20
+ * `--nome valor` e `--nome=valor`. `--arquivo` e `--vai-publicar` repetidos somam, e o que vem
21
+ * solto logo depois da lista é mais um item dela. `desconhecida`: a primeira opção que o script
22
+ * não conhece, ou null.
23
+ */
24
+ export function lerArgs(argv) {
25
+ const args = { arquivos: [], vaiPublicar: [], ajuda: false, desconhecida: null };
26
+ let naLista = false;
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const [, nome, colado] = argv[i].match(OPCAO) ?? [];
29
+ const solto = !nome && !argv[i].startsWith('--');
30
+ if (nome) guardar(args, nome, colado ?? (i + 1 < argv.length && !argv[i + 1].startsWith('--') ? argv[++i] : ''));
31
+ else if (argv[i] === '--ajuda') args.ajuda = true;
32
+ else if (solto && naLista) args.arquivos.push(argv[i]);
33
+ else args.desconhecida ??= argv[i];
34
+ naLista = nome ? nome === 'arquivo' : solto && naLista;
35
+ }
36
+ return args;
37
+ }
38
+
39
+ /** A lista de `--arquivo` → `[{ arquivo, formato }]` (`formato` null quando não veio `=formato`). */
40
+ export function lerLista(arquivos) {
41
+ const textos = arquivos.join(',').split(',').map((s) => s.trim()).filter(Boolean);
42
+ return textos.map(lerItemDaLista).map((i) => (typeof i === 'string' ? { arquivo: i, formato: null } : i));
43
+ }
44
+
45
+ /** O `--run` é um segmento só: sem `/`, `\` nem `..`. */
46
+ export const runValido = (run) => !/[\\/]/.test(run) && run !== '..' && run !== '.';
@@ -0,0 +1,51 @@
1
+ // Canais da entrega: a pasta de cada formato (o `platform:` do best-practice), os nomes que o
2
+ // LEIA-ME mostra e o que nunca entra numa entrega.
3
+ // Spec: fase-u3a1-pasta-de-entrega.md, regras 1 e 3 (repositório do OpenCrew).
4
+ import { existsSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { lerFrontmatter, lerTexto } from '../verificar/leitura.mjs';
7
+
8
+ /** Pasta do canal → nome no LEIA-ME, na ordem em que as seções aparecem. */
9
+ export const CANAIS = { instagram: 'Instagram', linkedin: 'LinkedIn', blog: 'Blog', email: 'E-mail', whatsapp: 'WhatsApp', twitter: 'X/Twitter', youtube: 'YouTube' };
10
+ export const OUTROS = 'outros';
11
+ export const EDITAVEIS = 'editaveis';
12
+ export const ehCanal = (pasta) => Object.hasOwn(CANAIS, pasta);
13
+ /** Como a pasta é chamada numa mensagem: o nome do canal, ou `outros/`. */
14
+ export const nomeDaPasta = (pasta) => CANAIS[pasta] ?? `${pasta}/`;
15
+
16
+ async function plataformaEm(raiz, pasta, formato) {
17
+ const arquivo = path.join(raiz, '_opencrew', ...pasta, `${formato}.md`);
18
+ if (!existsSync(arquivo)) return null;
19
+ const valor = lerFrontmatter(await lerTexto(arquivo))?.platform;
20
+ return typeof valor === 'string' && valor.trim() ? valor.trim().toLowerCase() : null;
21
+ }
22
+
23
+ /**
24
+ * Canal de um formato: o `platform:` de `_opencrew/best-practices.local/<formato>.md`, quando o
25
+ * arquivo o declara; senão, o do core. Sem formato, sem best-practice, sem `platform:` ou com
26
+ * plataforma que não é uma das sete pastas: null (o arquivo vai para `outros/`).
27
+ */
28
+ export async function canalDoFormato(raiz, formato) {
29
+ if (!formato || !/^[a-z0-9-]+$/.test(formato)) return null;
30
+ const local = await plataformaEm(raiz, ['best-practices.local'], formato);
31
+ const plataforma = local ?? (await plataformaEm(raiz, ['core', 'best-practices'], formato));
32
+ return plataforma && ehCanal(plataforma) ? plataforma : null;
33
+ }
34
+
35
+ const PASTAS_DE_SERVICO = new Set(['entrega', 'entrega.tmp', 'export']);
36
+
37
+ /**
38
+ * Arquivo de serviço nunca entra, mesmo listado: `verificacao-*.md`, `publicado.json`, o que está
39
+ * em `entrega/`, `entrega.tmp/` ou `export/` de uma execução e o `caption.txt` direto na pasta
40
+ * da execução (é ali que o publicador o grava; dentro de uma pasta `vN` ele é saída de passo).
41
+ * @param {string} rel caminho relativo ao projeto, com `/`
42
+ */
43
+ export function ehDeServico(rel) {
44
+ const partes = rel.toLowerCase().split('/');
45
+ const nome = partes.at(-1);
46
+ if (/^verificacao-.*\.md$/.test(nome) || nome === 'publicado.json') return true;
47
+ const naSaida = partes[0] === 'crews' && partes[2] === 'output';
48
+ if (!naSaida) return false;
49
+ if (partes.slice(4, -1).some((p) => PASTAS_DE_SERVICO.has(p))) return true;
50
+ return nome === 'caption.txt' && partes.length === 5;
51
+ }
@@ -0,0 +1,49 @@
1
+ // Blocos de rótulo do arquivo de origem que não chegam à entrega: os de serviço (notas, checklist,
2
+ // FORMAT) e, nos formatos lidos pelo leitor de peças, os que não são a peça do formato (os SLIDES
3
+ // de um carrossel, por exemplo). A entrega não muda: o LEIA-ME só passa a dizer o que ficou fora.
4
+ // Spec: fase-u3a1-pasta-de-entrega.md, §4 e §6, ajuste da execução real (repositório do OpenCrew).
5
+ import { semFrontmatter } from '../verificar/leitura.mjs';
6
+ import { ROTULOS } from '../verificar/pecas.mjs';
7
+ import { lerTrechos, marcar } from '../verificar/secoes.mjs';
8
+ import { PRINCIPAL } from './leitor.mjs';
9
+ import { ehServico, secoesDeRotulo, semComentarios } from './texto.mjs';
10
+
11
+ export const MSG = {
12
+ fora: (blocos) => `Ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem.`,
13
+ foraNaTela: (arquivo, blocos) => `${arquivo}: ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem.`,
14
+ };
15
+
16
+ const tabelaDe = (formato) => (formato === 'twitter-thread' ? 'twitter-post' : formato);
17
+
18
+ /** O bloco deste rótulo chega à entrega? `formato`: o do leitor de peças, ou null (só o de serviço sai). */
19
+ function entra(rotulo, formato) {
20
+ if (ehServico(rotulo)) return false;
21
+ if (!formato) return true;
22
+ const papel = ROTULOS[tabelaDe(formato)]?.[rotulo];
23
+ return papel === PRINCIPAL[formato] || papel === 'parte' || rotulo === 'HASHTAGS';
24
+ }
25
+
26
+ /** As seções de rótulo como o leitor de peças as vê: cada uma vai até o próximo rótulo ou cabeçalho de peça. */
27
+ function secoesDoLeitor(corpo, formato) {
28
+ const trechos = lerTrechos(marcar(corpo), tabelaDe(formato)).filter((t) => t.rotulo);
29
+ return trechos.map((t) => ({ rotulo: t.rotulo, texto: t.linhas.join('\n').trim() }));
30
+ }
31
+
32
+ /**
33
+ * Rótulos dos blocos com texto que ficam fora da entrega, cada um uma vez, na ordem do arquivo.
34
+ * @param {string} texto o arquivo de origem, sem BOM e com LF
35
+ * @param {string|null} [formato] formato lido pelo leitor de peças (`instagram-feed`,
36
+ * `linkedin-post`, `twitter-post`, `twitter-thread`); null nos outros, em que só o bloco de
37
+ * serviço fica fora
38
+ * @returns {string[]}
39
+ */
40
+ export function blocosFora(texto, formato = null) {
41
+ const corpo = semComentarios(semFrontmatter(texto));
42
+ const secoes = formato ? secoesDoLeitor(corpo, formato) : secoesDeRotulo(corpo);
43
+ return [...new Set(secoes.filter((s) => s.rotulo && s.texto && !entra(s.rotulo, formato)).map((s) => s.rotulo))];
44
+ }
45
+
46
+ /** O aviso de um arquivo: `{ pasta, texto, tela }`, ou nenhum quando nada ficou fora. */
47
+ export function avisoDeFora(item, blocos) {
48
+ return blocos.length ? [{ pasta: item.canal, texto: MSG.fora(blocos), tela: MSG.foraNaTela(item.rel, blocos) }] : [];
49
+ }
@@ -0,0 +1,83 @@
1
+ // Gravação da entrega: montada em `entrega.tmp/`, ao lado, e trocada no fim. Nada pela metade:
2
+ // em qualquer falha a entrega anterior fica como estava e não sobra pasta temporária. O script só
3
+ // apaga a própria `entrega/` e os temporários que ele mesmo criou.
4
+ // Spec: fase-u3a1-pasta-de-entrega.md, regras 12 e 15 (repositório do OpenCrew).
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ const apagar = (pasta) => fs.rm(pasta, { recursive: true, force: true, maxRetries: 3 });
9
+
10
+ /** 'pasta', 'arquivo' ou null (não existe). */
11
+ async function tipoDe(caminho) {
12
+ try {
13
+ return (await fs.stat(caminho)).isDirectory() ? 'pasta' : 'arquivo';
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /** Temporário que sobrou de uma chamada interrompida: a pasta é do script; arquivo com o mesmo nome não é. */
20
+ async function limparSobras({ entrega, tmp, antiga }) {
21
+ if ((await tipoDe(antiga)) === 'pasta') {
22
+ if (await tipoDe(entrega)) await apagar(antiga);
23
+ else await fs.rename(antiga, entrega); // a troca anterior parou no meio: a entrega volta
24
+ }
25
+ if ((await tipoDe(tmp)) === 'pasta') await apagar(tmp);
26
+ }
27
+
28
+ /** Grava um arquivo da entrega na pasta temporária: o texto gerado, ou os mesmos bytes da origem. */
29
+ async function gravarArquivo(destino, a) {
30
+ await fs.mkdir(path.dirname(destino), { recursive: true });
31
+ if (a.texto != null) await fs.writeFile(destino, a.texto, 'utf8');
32
+ else await fs.copyFile(a.de, destino);
33
+ }
34
+
35
+ /** Põe a pasta nova no lugar da anterior; se a nova não entra, a anterior volta. */
36
+ async function trocar({ entrega, tmp, antiga }, passo) {
37
+ const anterior = await tipoDe(entrega);
38
+ passo(entrega);
39
+ if (anterior === 'arquivo') throw new Error('entrega não é uma pasta');
40
+ if (anterior) await fs.rename(entrega, antiga);
41
+ try {
42
+ await fs.rename(tmp, entrega);
43
+ } catch (erro) {
44
+ if (anterior) await fs.rename(antiga, entrega);
45
+ throw erro;
46
+ }
47
+ passo(antiga);
48
+ if (anterior) await apagar(antiga);
49
+ }
50
+
51
+ /**
52
+ * Refaz `entrega/` do zero e grava, ao lado, o relatório da verificação.
53
+ * @param {string} execucao pasta da execução (`crews/<crew>/output/<run>/`), absoluta
54
+ * @param {object[]} arquivos `{ pasta, nome, texto | de }`
55
+ * @param {{ leiame: string, relatorio: string }} textos
56
+ * @returns {Promise<string|null>} null quando gravou tudo; senão, o caminho que não foi gravado
57
+ */
58
+ export async function gravarEntrega(execucao, arquivos, { leiame, relatorio }) {
59
+ const pastas = { entrega: path.join(execucao, 'entrega'), tmp: path.join(execucao, 'entrega.tmp'), antiga: path.join(execucao, 'entrega.antiga.tmp') };
60
+ let alvo = pastas.tmp;
61
+ let criei = false;
62
+ const passo = (caminho) => { alvo = caminho; };
63
+ try {
64
+ await limparSobras(pastas);
65
+ await fs.mkdir(pastas.tmp);
66
+ criei = true;
67
+ for (const a of arquivos) {
68
+ passo(path.join(pastas.tmp, a.pasta, a.nome));
69
+ await gravarArquivo(alvo, a);
70
+ }
71
+ passo(path.join(pastas.tmp, 'LEIA-ME.md'));
72
+ await fs.writeFile(alvo, leiame, 'utf8');
73
+ await trocar(pastas, passo);
74
+ criei = false;
75
+ passo(path.join(execucao, 'verificacao-entrega.md'));
76
+ await fs.writeFile(alvo, relatorio, 'utf8');
77
+ return null;
78
+ } catch {
79
+ if (criei) await apagar(pastas.tmp).catch(() => {});
80
+ // O arquivo que falhou dentro da pasta temporária é citado pelo lugar em que ficaria.
81
+ return alvo.startsWith(pastas.tmp + path.sep) ? path.join(pastas.entrega, alvo.slice(pastas.tmp.length + 1)) : alvo;
82
+ }
83
+ }