@aksp/opencrew 1.4.2 → 1.6.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 +63 -0
- package/README.md +23 -11
- package/package.json +1 -1
- package/src/commands/init.js +20 -16
- package/src/commands/update.js +58 -42
- package/src/lib/ides.js +15 -12
- package/src/lib/manifest.js +89 -0
- package/src/lib/migrations.js +110 -0
- package/templates/.mcp.json +1 -1
- package/templates/AGENTS.md +2 -1
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/best-practices/copywriting.md +4 -1
- package/templates/_opencrew/core/best-practices/image-design.md +5 -5
- package/templates/_opencrew/core/best-practices/instagram-feed.md +4 -4
- package/templates/_opencrew/core/best-practices/instagram-reels.md +1 -1
- package/templates/_opencrew/core/best-practices/review.md +7 -0
- package/templates/_opencrew/core/best-practices/social-networks-publishing.md +8 -8
- package/templates/_opencrew/core/prompts/build.prompt.md +16 -0
- package/templates/_opencrew/core/prompts/discovery.prompt.md +15 -2
- package/templates/_opencrew/core/runner.pipeline.md +86 -11
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +189 -0
- package/templates/_opencrew/core/scripts/verificar/leitura.mjs +104 -0
- package/templates/_opencrew/core/scripts/verificar/regras.mjs +127 -0
- package/templates/_opencrew/core/scripts/verificar.mjs +118 -0
- package/templates/skills/image-ai-generator/SKILL.md +1 -1
- package/templates/skills/image-creator/SKILL.md +2 -2
- package/templates/skills/image-fetcher/SKILL.md +1 -1
- package/templates/skills/opencrew-best-practice-creator/SKILL.md +4 -4
- package/templates/skills/template-designer/SKILL.md +3 -3
- package/templates/skills/template-designer/base-templates/model-a.html +1 -1
- package/templates/skills/template-designer/base-templates/model-b.html +1 -1
- package/templates/skills/template-designer/base-templates/model-c.html +1 -1
package/templates/AGENTS.md
CHANGED
|
@@ -115,4 +115,5 @@ enabled, it writes `crews/{name}/state.json` before each step and at every hando
|
|
|
115
115
|
- ALWAYS save outputs to the crew's output directory
|
|
116
116
|
- When switching personas (inline execution), clearly indicate which agent is speaking
|
|
117
117
|
- When using subagents, inform the user that background work is happening
|
|
118
|
-
-
|
|
118
|
+
- Crew memory (memories.md) records only the user's explicit feedback and corrections —
|
|
119
|
+
written at the checkpoint where they happen (see the Pipeline Runner), never invented learnings
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.6.0
|
|
@@ -17,7 +17,10 @@ version: "1.0.0"
|
|
|
17
17
|
5. Present 3 distinct hook options before drafting the body.
|
|
18
18
|
6. Align completely with brand voice and audience-specific vocabulary.
|
|
19
19
|
7. Write concise, one-idea sentences and short paragraphs.
|
|
20
|
-
8. Use specific numbers and concrete details instead of vague claims
|
|
20
|
+
8. Use specific numbers and concrete details instead of vague claims — but only REAL ones
|
|
21
|
+
(briefing, research with source, company profile). Never invent cases, testimonials, clients,
|
|
22
|
+
numbers or first-person stories: nunca invente; write `[PREENCHER: o que falta]` instead and
|
|
23
|
+
the user fills it in at final approval.
|
|
21
24
|
9. Select one dominant psychological driver and anchor the piece to it.
|
|
22
25
|
10. Deploy the appropriate framework (AIDA, PAS, BAB) based on the funnel stage.
|
|
23
26
|
11. Inject an objection neutralizer immediately before the CTA.
|
|
@@ -88,7 +88,7 @@ Present all rendered images to the user or downstream agent. Include the design
|
|
|
88
88
|
## Platform Specifications
|
|
89
89
|
|
|
90
90
|
### Instagram Post / Carousel
|
|
91
|
-
- **Viewport**: 1080 x
|
|
91
|
+
- **Viewport**: 1080 x 1350 (4:5 portrait)
|
|
92
92
|
- **Min font sizes**: Hero 58px, Heading 43px, Body 34px, Caption 24px
|
|
93
93
|
- **Optimal slide count**: 5-10 slides. Under 5 feels incomplete, over 10 causes drop-off.
|
|
94
94
|
- **Structure**: Hook on slide 1, CTA on last slide, value in between.
|
|
@@ -135,7 +135,7 @@ Present all rendered images to the user or downstream agent. Include the design
|
|
|
135
135
|
DESIGN SYSTEM
|
|
136
136
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
137
137
|
Platform: Instagram Carousel
|
|
138
|
-
Viewport: 1080 x
|
|
138
|
+
Viewport: 1080 x 1350
|
|
139
139
|
Slides: 7 (hook + 5 content + CTA)
|
|
140
140
|
|
|
141
141
|
Colors:
|
|
@@ -181,7 +181,7 @@ File: slide-01.html
|
|
|
181
181
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
|
182
182
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
183
183
|
body {
|
|
184
|
-
width: 1080px; height:
|
|
184
|
+
width: 1080px; height: 1350px; overflow: hidden;
|
|
185
185
|
background: #1A1A2E;
|
|
186
186
|
font-family: 'Inter', sans-serif;
|
|
187
187
|
display: flex; flex-direction: column;
|
|
@@ -342,7 +342,7 @@ Design rationale: Clean white background matches LinkedIn's professional aesthet
|
|
|
342
342
|
|
|
343
343
|
3. **Document design rationale.** After each completed design, briefly explain why you made the key visual choices: color rationale, font selection, layout strategy. This helps the user understand the design thinking and makes iteration faster.
|
|
344
344
|
|
|
345
|
-
4. **Match viewport exactly.** Body width and height in CSS must match the browser viewport resize dimensions exactly. A
|
|
345
|
+
4. **Match viewport exactly.** Body width and height in CSS must match the browser viewport resize dimensions exactly. A 1080x1350 carousel slide means body { width: 1080px; height: 1350px; }.
|
|
346
346
|
|
|
347
347
|
## Vocabulary Guidance
|
|
348
348
|
|
|
@@ -350,7 +350,7 @@ Design rationale: Clean white background matches LinkedIn's professional aesthet
|
|
|
350
350
|
|
|
351
351
|
- **"Design system"**: The foundational term for consistent visual identity across pieces. Always define it before creating individual assets.
|
|
352
352
|
- **"Visual hierarchy"**: How the eye moves through the design. Use this when explaining font size, weight, and positioning choices.
|
|
353
|
-
- **"Viewport: WxH"**: Always state the target dimensions explicitly. "Instagram carousel at
|
|
353
|
+
- **"Viewport: WxH"**: Always state the target dimensions explicitly. "Instagram carousel at 1080x1350" not "standard Instagram size."
|
|
354
354
|
- **"Contrast ratio"**: Reference WCAG contrast standards when justifying color combinations. "4.5:1 minimum for body text."
|
|
355
355
|
- **"Self-contained HTML"**: The non-negotiable constraint. Reinforce that every file must render independently without external dependencies.
|
|
356
356
|
- **"Rendering verification"**: The step where you visually confirm the screenshot matches the intended design before proceeding.
|
|
@@ -8,14 +8,14 @@ whenToUse: |
|
|
|
8
8
|
constraints:
|
|
9
9
|
caption_max_chars: 2200
|
|
10
10
|
caption_visible_chars: 125
|
|
11
|
-
|
|
11
|
+
hashtags_max: 30
|
|
12
12
|
recommended_hashtags: "5-15"
|
|
13
|
-
carousel_max_slides:
|
|
13
|
+
carousel_max_slides: 10
|
|
14
14
|
recommended_slides: "8-10"
|
|
15
15
|
min_words_per_slide: 40
|
|
16
16
|
max_words_per_slide: 80
|
|
17
|
-
image_ratio: "
|
|
18
|
-
image_resolution: "
|
|
17
|
+
image_ratio: "4:5 portrait"
|
|
18
|
+
image_resolution: "1080x1350px"
|
|
19
19
|
version: "1.0.0"
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -22,6 +22,13 @@ version: "1.0.0"
|
|
|
22
22
|
10. Assign an APPROVE verdict only if overall score is >= 7/10 and no criterion is < 4/10.
|
|
23
23
|
11. Include at least one acknowledged strength, even in REJECT reviews.
|
|
24
24
|
12. Provide a specific fix or rewrite example for every required change.
|
|
25
|
+
13. **Números medidos, nunca estimados** — nunca estime contagens (caracteres, hashtags, links,
|
|
26
|
+
slides): copie os valores do relatório `--- VERIFICAÇÃO AUTOMÁTICA ---`. Checklist só marca ✓
|
|
27
|
+
o que o relatório confirma.
|
|
28
|
+
14. **Sem APPROVE com bloqueio** — qualquer bloqueio no relatório da verificação automática é
|
|
29
|
+
REJECT, seja qual for a nota (exceto `[PREENCHER]`, que vai para o usuário na aprovação final).
|
|
30
|
+
15. **Alerta limita a nota** — com alerta não resolvido nem justificado, a nota máxima é 7/10;
|
|
31
|
+
liste cada alerta e diga se foi resolvido.
|
|
25
32
|
|
|
26
33
|
<!-- End Compact Rules. Full reference below. -->
|
|
27
34
|
|
|
@@ -150,13 +150,13 @@ Platform: Instagram (carousel)
|
|
|
150
150
|
Account: @brandname
|
|
151
151
|
Skill: instagram-publisher
|
|
152
152
|
Images: 7 slides
|
|
153
|
-
1. slide-01.jpg (
|
|
154
|
-
2. slide-02.jpg (
|
|
155
|
-
3. slide-03.jpg (
|
|
156
|
-
4. slide-04.jpg (
|
|
157
|
-
5. slide-05.jpg (
|
|
158
|
-
6. slide-06.jpg (
|
|
159
|
-
7. slide-07.jpg (
|
|
153
|
+
1. slide-01.jpg (1080x1350, JPEG, 287KB)
|
|
154
|
+
2. slide-02.jpg (1080x1350, JPEG, 195KB)
|
|
155
|
+
3. slide-03.jpg (1080x1350, JPEG, 213KB)
|
|
156
|
+
4. slide-04.jpg (1080x1350, JPEG, 178KB)
|
|
157
|
+
5. slide-05.jpg (1080x1350, JPEG, 201KB)
|
|
158
|
+
6. slide-06.jpg (1080x1350, JPEG, 192KB)
|
|
159
|
+
7. slide-07.jpg (1080x1350, JPEG, 244KB)
|
|
160
160
|
|
|
161
161
|
Caption (1,847 / 2,200 chars):
|
|
162
162
|
"You are doing 100 things to grow on Instagram.
|
|
@@ -173,7 +173,7 @@ Caption (1,847 / 2,200 chars):
|
|
|
173
173
|
VALIDATION
|
|
174
174
|
Image format: JPEG (required: JPEG)
|
|
175
175
|
Image count: 7 (required: 2-10)
|
|
176
|
-
Image dimensions:
|
|
176
|
+
Image dimensions: 1080x1350 (valid carousel)
|
|
177
177
|
Caption length: 1,847 chars (max: 2,200)
|
|
178
178
|
Hashtags: 5 (recommended: 5-8)
|
|
179
179
|
Rate limit: 3/25 posts used in last 24h
|
|
@@ -90,6 +90,18 @@ Generate these files. Use the Write tool for all file creation — never use Bas
|
|
|
90
90
|
- pipeline/data/anti-patterns.md
|
|
91
91
|
- pipeline/data/tone-of-voice.md # for content crews
|
|
92
92
|
```
|
|
93
|
+
- Include a `fontes:` section with the project sources from `discovery.yaml →
|
|
94
|
+
project_sources` (omit it only if that list is empty). The Pipeline Runner reads them at the
|
|
95
|
+
start of every run and checks they still exist:
|
|
96
|
+
```yaml
|
|
97
|
+
fontes:
|
|
98
|
+
- caminho: Memoria/01_Decisoes.md # relative to the project root
|
|
99
|
+
para_que: decisões de público e posicionamento
|
|
100
|
+
```
|
|
101
|
+
- **Paths to the user's project files** — in `crew.yaml`, step files and tasks — are always
|
|
102
|
+
written as a caminho relativo à raiz do projeto (relative to the project root), between
|
|
103
|
+
backticks, e.g. `` `Ativos/Identidade Visual/logo.png` ``. NEVER write absolute paths
|
|
104
|
+
(`C:/…`, `J:/…`, `/Users/…`): they break as soon as the user moves or syncs the folder.
|
|
93
105
|
- Include an `agent_dependencies:` section (OPTIONAL — enables runtime
|
|
94
106
|
Pre-Execution Agent Selection):
|
|
95
107
|
```yaml
|
|
@@ -271,6 +283,10 @@ with placeholders. 1 example acceptable if it is comprehensive; 2 preferred if s
|
|
|
271
283
|
3. [Specific mistake]: [Why it's harmful]
|
|
272
284
|
4. [Specific mistake]: [Why it's harmful]
|
|
273
285
|
(Minimum 4 items. Each sourced from research on common domain mistakes.)
|
|
286
|
+
**Every agent that writes content** (writer, copywriter, creator, consultant, strategist) MUST
|
|
287
|
+
also include this item, verbatim in meaning: "Never invent cases, testimonials, clients, numbers,
|
|
288
|
+
dates or first-person stories — nunca invente; when real data is missing, write
|
|
289
|
+
`[PREENCHER: o que falta]` so the user fills it in at final approval."
|
|
274
290
|
|
|
275
291
|
### Always Do
|
|
276
292
|
1. [Specific positive practice]: [Why it matters]
|
|
@@ -117,6 +117,14 @@ Based on the detected domain, ask the most relevant contextual question first. W
|
|
|
117
117
|
**If domain = `mixed`:**
|
|
118
118
|
Ask the most pressing question from each relevant domain, starting with the primary one. Cap at 3 questions total in this step.
|
|
119
119
|
|
|
120
|
+
**Always (any domain) — project sources (fontes):** ask ONE question:
|
|
121
|
+
"Tem arquivos ou pastas deste projeto que a crew deve consultar sempre? (por exemplo: decisões,
|
|
122
|
+
calendário de eventos, manual de marca, pasta de logos). Pode citar o caminho ou o nome."
|
|
123
|
+
If the user names files/folders, confirm each one exists (search the project if only a name was
|
|
124
|
+
given) and store them in `project_sources` with paths **relative to the project root** (never
|
|
125
|
+
absolute — absolute paths break when the folder is moved or synced to another computer).
|
|
126
|
+
If the user says no, store an empty list.
|
|
127
|
+
|
|
120
128
|
---
|
|
121
129
|
|
|
122
130
|
### Step 4 — Tools and Integrations (automatic)
|
|
@@ -234,12 +242,17 @@ Wait for confirmation before writing the output file.
|
|
|
234
242
|
|
|
235
243
|
---
|
|
236
244
|
|
|
237
|
-
## Output: `_build/discovery.yaml`
|
|
245
|
+
## Output: `crews/{code}/_build/discovery.yaml`
|
|
238
246
|
|
|
239
|
-
After the user confirms in Step 7, write the following file
|
|
247
|
+
After the user confirms in Step 7, write the following file **inside the crew folder** —
|
|
248
|
+
`crews/{code}/_build/discovery.yaml` (never at the project root; `{code}` = the unique
|
|
249
|
+
`crew_code` below):
|
|
240
250
|
|
|
241
251
|
```yaml
|
|
242
252
|
crew_code: "{slugified crew name from purpose}"
|
|
253
|
+
project_sources: # relative to the project root; becomes `fontes:` in crew.yaml
|
|
254
|
+
- path: "{e.g. Memoria/01_Decisoes.md}"
|
|
255
|
+
purpose: "{what the crew uses it for}"
|
|
243
256
|
purpose: "{user's description from Step 1}"
|
|
244
257
|
domain: "{content | research | automation | analysis | mixed}"
|
|
245
258
|
# When a template was used (Step 0), these fields are populated from discovery.template.yaml:
|
|
@@ -52,8 +52,12 @@ Before starting execution:
|
|
|
52
52
|
[ -f crews/{name}/_memory/memories.md ] && grep -q "## Estilo de Escrita" crews/{name}/_memory/memories.md && echo "NEW_FORMAT" || echo "OLD_FORMAT"
|
|
53
53
|
```
|
|
54
54
|
- If `NEW_FORMAT` → proceed normally.
|
|
55
|
-
- If `OLD_FORMAT` (or file is empty / does not exist) →
|
|
56
|
-
|
|
55
|
+
- If `OLD_FORMAT` (or file is empty / does not exist) → migrate before proceeding:
|
|
56
|
+
a0. If the file exists and is not empty, FIRST copy it to `crews/{name}/_memory/memories.md.bak`
|
|
57
|
+
(never lose what the crew learned), then tell the user in one line:
|
|
58
|
+
"Atualizei o formato da memória da crew; a versão anterior está em `memories.md.bak`."
|
|
59
|
+
Move every rule you can recognize from the old file into the matching new section.
|
|
60
|
+
a. Write `crews/{name}/_memory/memories.md` with the new sections format:
|
|
57
61
|
```markdown
|
|
58
62
|
# Crew Memory: {crew-name}
|
|
59
63
|
|
|
@@ -79,7 +83,31 @@ Before starting execution:
|
|
|
79
83
|
| Data | Run ID | Tema | Output | Score | Resultado |
|
|
80
84
|
|------|--------|------|--------|-------|-----------|
|
|
81
85
|
```
|
|
82
|
-
- Do
|
|
86
|
+
- Do not pause execution for this migration (the one-line notice above is enough).
|
|
87
|
+
|
|
88
|
+
1c. **Project sources (`fontes:`)** — if `crew.yaml` has a `fontes:` list (files or folders of
|
|
89
|
+
the user's project, paths relative to the project root), read them now: a file in full up to
|
|
90
|
+
~300 lines, otherwise its headings plus the passages relevant to this run's task; a folder as
|
|
91
|
+
its file list. Treat them as the **truth of the project**: when they disagree with the
|
|
92
|
+
briefing, the research or your own assumptions, the sources take precedence over them
|
|
93
|
+
(as fontes valem sobre o briefing e a pesquisa) — and say so when it matters.
|
|
94
|
+
|
|
95
|
+
1d. **Source check** — before the first step, run:
|
|
96
|
+
```bash
|
|
97
|
+
node _opencrew/core/scripts/conferir-fontes.mjs --crew crews/{name}
|
|
98
|
+
```
|
|
99
|
+
If the last line is `FONTES:PENDENTE` (a cited file was moved, renamed or deleted), show the
|
|
100
|
+
report and ask — never continue silently with a missing source:
|
|
101
|
+
```
|
|
102
|
+
Alguns arquivos que a crew usa não estão mais onde ela espera:
|
|
103
|
+
{resumo do relatório}
|
|
104
|
+
|
|
105
|
+
1. Corrigir os caminhos sugeridos (troco nos arquivos da crew e guardo .bak)
|
|
106
|
+
2. Seguir assim mesmo
|
|
107
|
+
3. Parar
|
|
108
|
+
```
|
|
109
|
+
On 1, run the same command with `--corrigir` and show the new result. Not-portable alerts
|
|
110
|
+
(absolute paths) are mentioned once, without stopping.
|
|
83
111
|
|
|
84
112
|
2. Read `crews/{name}/pipeline/pipeline.yaml` for the pipeline definition
|
|
85
113
|
3. **Resolve skills**: Read `crew.yaml` → `skills` section. For each non-native skill (anything other than web_search, web_fetch):
|
|
@@ -261,8 +289,10 @@ Before executing any step that references an agent:
|
|
|
261
289
|
- The agent must follow the export process for the specified format — read the input file,
|
|
262
290
|
transform the content, and write the output file in the target format.
|
|
263
291
|
- Skip the best-practices lookup below for export formats.
|
|
264
|
-
b. **Content formats** — otherwise, read `_opencrew/
|
|
265
|
-
|
|
292
|
+
b. **Content formats** — otherwise, read `_opencrew/best-practices.local/{format}.md` (the user's
|
|
293
|
+
own version, never touched by `update`) if it exists, else `_opencrew/core/best-practices/{format}.md`
|
|
294
|
+
(e.g., `_opencrew/core/best-practices/instagram-feed.md`)
|
|
295
|
+
- If neither exists → **WARNING**: "Format '{format}' not found in _opencrew/best-practices.local/ or _opencrew/core/best-practices/. Skipping format injection." Continue without format.
|
|
266
296
|
c. Parse the YAML frontmatter to extract the `name` field
|
|
267
297
|
d. Extract the Markdown body (everything after the YAML frontmatter closing `---`)
|
|
268
298
|
e. Append to the agent's context, before skill instructions:
|
|
@@ -310,6 +340,15 @@ Before executing any step that references an agent:
|
|
|
310
340
|
c. Inject this block immediately after the agent definition and BEFORE format/skill context.
|
|
311
341
|
d. Skip sections that are empty or not relevant to the current agent (e.g., skip Design Visual for a writer agent).
|
|
312
342
|
e. If `memories.md` has no accumulated rules → skip injection entirely (no empty block).
|
|
343
|
+
f. **Truthfulness block (always)** — for every agent step that is not a checkpoint and not a
|
|
344
|
+
review step (no `on_reject:`), inject right after the crew memory block:
|
|
345
|
+
```
|
|
346
|
+
--- REGRAS DE VERACIDADE ---
|
|
347
|
+
- Nunca invente casos, depoimentos, clientes, números, datas ou histórias em 1ª pessoa.
|
|
348
|
+
- Use só fatos do briefing, da pesquisa (com fonte) ou do perfil da empresa.
|
|
349
|
+
- Faltou um dado real? Escreva [PREENCHER: o que falta] no lugar — o usuário completa
|
|
350
|
+
na aprovação final. Um [PREENCHER] honesto vale mais que um exemplo inventado.
|
|
351
|
+
```
|
|
313
352
|
|
|
314
353
|
### Context Compression (Summary-Based Handoff)
|
|
315
354
|
|
|
@@ -521,6 +560,14 @@ Apply this transformation consistently for every write in this step.
|
|
|
521
560
|
- **Always include the file path** of any generated content the user needs to review. Example: "Review the content at `crews/{name}/output/{run_id}/v1/content.md` and let me know if it looks good."
|
|
522
561
|
- Wait for user input before proceeding
|
|
523
562
|
- Save the user's choice/response for the next step
|
|
563
|
+
- **Correction → memory, right away**: if the answer corrects something (tone, audience, a term,
|
|
564
|
+
a fact, a format), write it to `crews/{name}/_memory/memories.md` in the matching section
|
|
565
|
+
**before the next step** (antes do próximo passo) — not only at the end of the run, which may
|
|
566
|
+
never come. A term the user asked to remove goes to `## Proibições Explícitas` **between
|
|
567
|
+
quotes** (entre aspas: `- Nunca usar "termo"`), so the automatic checker blocks it next time.
|
|
568
|
+
- **Correction vs. company profile**: if the correction contradicts `_opencrew/_memory/company.md`
|
|
569
|
+
(e.g. the organization's name, the main audience), ask: "Isso vale para todas as crews?
|
|
570
|
+
Atualizo o perfil da empresa?" — change `company.md` only after a yes.
|
|
524
571
|
- **If the step frontmatter contains `outputFile`**: after collecting the user's full response,
|
|
525
572
|
apply the Output Path Transformation **Step 1 only** (run_id injection — skip Step 2, version folder) to the `outputFile` path, then write the response to the transformed path using the Write tool before moving to the next step. Checkpoint files are user input captures, not versioned output — Step 2 does not apply here, regardless of the general "every write" rule in the Output Path Transformation section above.
|
|
526
573
|
Use this format:
|
|
@@ -628,11 +675,39 @@ catching obvious issues early and reducing review cycle waste.
|
|
|
628
675
|
|
|
629
676
|
### Review Loops
|
|
630
677
|
|
|
631
|
-
When a step has `on_reject: {step-id}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
-
|
|
635
|
-
|
|
678
|
+
When a step has `on_reject: {step-id}` (a review step):
|
|
679
|
+
|
|
680
|
+
1. **Automatic check BEFORE the reviewer runs** — run the checker on **all outputs** (todas as
|
|
681
|
+
saídas) of every non-checkpoint step from the `on_reject` step up to the step right before
|
|
682
|
+
the review, using the transformed paths of this run (run_id/vN):
|
|
683
|
+
```bash
|
|
684
|
+
node _opencrew/core/scripts/verificar.mjs --crew crews/{name} --arquivo "{path1},{path2},…" --formato {blog format id of those steps, if any}
|
|
685
|
+
```
|
|
686
|
+
Save the full output to `crews/{name}/output/{run_id}/verificacao-ciclo-{N}.md` and inject it
|
|
687
|
+
into the reviewer's context as `--- VERIFICAÇÃO AUTOMÁTICA ---`. The reviewer must copy the
|
|
688
|
+
measured values from it (see best-practices `review.md`). If the command itself fails (no
|
|
689
|
+
Node, unexpected error), tell the user "⚠️ A verificação automática não rodou: {motivo}" and
|
|
690
|
+
continue with the normal review.
|
|
691
|
+
2. **A block cannot be approved** — if the last line of the checker output is
|
|
692
|
+
`VERIFICACAO:BLOQUEADA`, the verdict is **REJECT** regardless of the score (qualquer que seja a
|
|
693
|
+
nota). Send the report (blocks first) to the writer together with the reviewer's feedback.
|
|
694
|
+
If the last line is `VERIFICACAO:AGUARDANDO_USUARIO`, the only blocks are `[PREENCHER: …]`
|
|
695
|
+
(real data only the user has): do NOT reject for them — the reviewer judges the rest, and the
|
|
696
|
+
final approval below collects the missing data from the user.
|
|
697
|
+
3. Track the review cycle count. If the reviewer rejects, go back to the referenced step.
|
|
698
|
+
4. If max_review_cycles is reached with blocks remaining, present the report to the user:
|
|
699
|
+
```
|
|
700
|
+
⚠️ A revisão ainda encontra bloqueios depois de {N} ciclos:
|
|
701
|
+
{lista de bloqueios do relatório}
|
|
702
|
+
|
|
703
|
+
1. Corrigir eu mesmo (eu edito o texto e você verifica de novo)
|
|
704
|
+
2. Aceitar assim mesmo (fica registrado no histórico da execução)
|
|
705
|
+
3. Abortar
|
|
706
|
+
```
|
|
707
|
+
5. **Final approval checkpoint** (the checkpoint after the review): show the summary of the last
|
|
708
|
+
report — `Verificação automática: {N} bloqueios, {M} alertas` — plus the list of alerts. If the
|
|
709
|
+
approved text still contains `[PREENCHER: …]`, ask the user for each missing piece of real
|
|
710
|
+
information and write it into the text before approving.
|
|
636
711
|
|
|
637
712
|
### Dashboard Handoff (between steps)
|
|
638
713
|
|
|
@@ -729,7 +804,7 @@ This archives the run state for the `runs` command while keeping crew history av
|
|
|
729
804
|
- Run scores, review grades, output file paths, topics from past runs
|
|
730
805
|
|
|
731
806
|
**Technical routing:** For any technical learning (bugs, workarounds, API behavior):
|
|
732
|
-
- If it affects any crew (Playwright bugs, OS rendering quirks, API limits) → write to
|
|
807
|
+
- If it affects any crew (Playwright bugs, OS rendering quirks, API limits) → write to `_opencrew/best-practices.local/{format}.md` instead of `memories.md` (copy the core file there first if the local one does not exist yet — the core folder is replaced by every `update`; the local one is never touched)
|
|
733
808
|
- If it is specific to this crew's output type or toolchain → add to `## Técnico (específico do crew)` following the dedup rules above
|
|
734
809
|
|
|
735
810
|
After applying all candidates, write the updated `memories.md`.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Conferência de fontes do OpenCrew — no início do run, confere se os arquivos que a crew cita
|
|
3
|
+
// existem. Se foram movidos, sugere o novo caminho (relativo à raiz do projeto); se o nome mudou,
|
|
4
|
+
// lista o que existe na pasta esperada. Nunca apaga nada; só corrige com --corrigir (e .bak).
|
|
5
|
+
// Uso: node _opencrew/core/scripts/conferir-fontes.mjs --crew crews/<nome> [--corrigir]
|
|
6
|
+
// Última linha da saída: FONTES:OK ou FONTES:PENDENTE (o runner lê esta linha).
|
|
7
|
+
// Spec: specs/fase-u2-crew-que-conhece-o-projeto.md (repositório do OpenCrew).
|
|
8
|
+
import { readFile, writeFile, readdir, copyFile } from 'node:fs/promises';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const IGNORAR = new Set(['node_modules', 'output', '_opencrew', '_build']);
|
|
14
|
+
const LIMITE_ENTRADAS = 20000;
|
|
15
|
+
|
|
16
|
+
const barra = (p) => p.split(path.sep).join('/');
|
|
17
|
+
const ehAbsoluto = (p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('/');
|
|
18
|
+
|
|
19
|
+
function pareceCaminho(t) {
|
|
20
|
+
if (/[{}<>*$|]/.test(t) || /^https?:/i.test(t) || !/[\\/]/.test(t)) return false;
|
|
21
|
+
return /\.[A-Za-z0-9]{1,5}$/.test(t) || /[\\/]$/.test(t);
|
|
22
|
+
}
|
|
23
|
+
const saidaDeRun = (t) => /(^|[\\/])(output|_build)[\\/]/.test(t);
|
|
24
|
+
|
|
25
|
+
/** Caminhos citados entre crases no crew.yaml e nos passos, mais as `fontes:` do crew.yaml. */
|
|
26
|
+
async function coletar(raiz, crew) {
|
|
27
|
+
const refs = new Map();
|
|
28
|
+
const add = (ref, arquivo) => {
|
|
29
|
+
if (!refs.has(ref)) refs.set(ref, new Set());
|
|
30
|
+
refs.get(ref).add(arquivo);
|
|
31
|
+
};
|
|
32
|
+
const crewYaml = path.join(raiz, crew, 'crew.yaml');
|
|
33
|
+
const passos = path.join(raiz, crew, 'pipeline', 'steps');
|
|
34
|
+
const arquivos = existsSync(crewYaml) ? [crewYaml] : [];
|
|
35
|
+
if (existsSync(passos)) {
|
|
36
|
+
for (const f of await readdir(passos)) if (f.endsWith('.md')) arquivos.push(path.join(passos, f));
|
|
37
|
+
}
|
|
38
|
+
for (const arquivo of arquivos) {
|
|
39
|
+
const texto = await readFile(arquivo, 'utf8');
|
|
40
|
+
for (const m of texto.matchAll(/`([^`\n]+)`/g)) {
|
|
41
|
+
const t = m[1].trim();
|
|
42
|
+
if (pareceCaminho(t) && !saidaDeRun(t)) add(t, arquivo);
|
|
43
|
+
}
|
|
44
|
+
if (arquivo === crewYaml) {
|
|
45
|
+
for (const m of texto.matchAll(/^\s*-?\s*caminho:\s*["']?([^"'\n#]+?)["']?\s*$/gm)) add(m[1].trim(), arquivo);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return refs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolver(raiz, crew, ref) {
|
|
52
|
+
if (ehAbsoluto(ref)) return existsSync(ref) ? path.resolve(ref) : null;
|
|
53
|
+
for (const base of [path.join(raiz, crew), raiz]) {
|
|
54
|
+
const p = path.resolve(base, ref);
|
|
55
|
+
if (existsSync(p)) return p;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Índice nome-do-arquivo → caminhos relativos, ignorando saídas, dependências e pastas ocultas. */
|
|
61
|
+
async function indexar(raiz) {
|
|
62
|
+
const porNome = new Map();
|
|
63
|
+
let n = 0;
|
|
64
|
+
async function walk(dir) {
|
|
65
|
+
let entradas;
|
|
66
|
+
try { entradas = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
67
|
+
for (const e of entradas) {
|
|
68
|
+
if (++n > LIMITE_ENTRADAS) return;
|
|
69
|
+
if (e.name.startsWith('.') || (e.isDirectory() && IGNORAR.has(e.name))) continue;
|
|
70
|
+
const abs = path.join(dir, e.name);
|
|
71
|
+
const chave = e.name.toLowerCase();
|
|
72
|
+
if (!porNome.has(chave)) porNome.set(chave, []);
|
|
73
|
+
porNome.get(chave).push(barra(path.relative(raiz, abs)) + (e.isDirectory() ? '/' : ''));
|
|
74
|
+
if (e.isDirectory()) await walk(abs);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await walk(raiz);
|
|
78
|
+
return porNome;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function conferir({ raiz, crew }) {
|
|
82
|
+
const refs = await coletar(raiz, crew);
|
|
83
|
+
let indice = null;
|
|
84
|
+
const lista = [];
|
|
85
|
+
for (const [ref, citado] of refs) {
|
|
86
|
+
const item = { ref, citadoEm: [...citado], estado: 'ok', sugestao: null, candidatos: [], pasta: [] };
|
|
87
|
+
const achado = resolver(raiz, crew, ref);
|
|
88
|
+
if (achado) {
|
|
89
|
+
if (ehAbsoluto(ref)) {
|
|
90
|
+
item.estado = 'nao-portatil';
|
|
91
|
+
const rel = path.relative(raiz, achado);
|
|
92
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel)) item.sugestao = barra(rel) + (/[\\/]$/.test(ref) ? '/' : '');
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
item.estado = 'faltando';
|
|
96
|
+
indice ??= await indexar(raiz);
|
|
97
|
+
const nome = path.basename(ref.replace(/[\\/]+$/, '')).toLowerCase();
|
|
98
|
+
item.candidatos = (indice.get(nome) ?? []).filter((c) => /[\\/]$/.test(ref) === c.endsWith('/'));
|
|
99
|
+
if (item.candidatos.length === 1) item.sugestao = item.candidatos[0];
|
|
100
|
+
if (!item.candidatos.length) {
|
|
101
|
+
const pai = resolver(raiz, crew, path.dirname(ref.replace(/[\\/]+$/, '')));
|
|
102
|
+
if (pai) {
|
|
103
|
+
try { item.pasta = (await readdir(pai)).filter((f) => !f.startsWith('.')); } catch { /* não é pasta */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
lista.push(item);
|
|
108
|
+
}
|
|
109
|
+
const status = lista.some((i) => i.estado === 'faltando') ? 'PENDENTE' : 'OK';
|
|
110
|
+
return { crew, raiz, refs: lista, status };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function copiaDeSeguranca(arquivo) {
|
|
114
|
+
const bak = existsSync(`${arquivo}.bak`) ? `${arquivo}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}` : `${arquivo}.bak`;
|
|
115
|
+
await copyFile(arquivo, bak);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Troca, nos arquivos da crew, cada caminho com sugestão única. @returns quantos caminhos */
|
|
119
|
+
export async function corrigir({ resultado }) {
|
|
120
|
+
const comSugestao = resultado.refs.filter((i) => i.sugestao && i.estado !== 'ok');
|
|
121
|
+
const tocados = new Set();
|
|
122
|
+
for (const item of comSugestao) {
|
|
123
|
+
for (const arquivo of item.citadoEm) {
|
|
124
|
+
const texto = await readFile(arquivo, 'utf8');
|
|
125
|
+
if (!tocados.has(arquivo)) {
|
|
126
|
+
await copiaDeSeguranca(arquivo);
|
|
127
|
+
tocados.add(arquivo);
|
|
128
|
+
}
|
|
129
|
+
await writeFile(arquivo, texto.split(item.ref).join(item.sugestao));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return comSugestao.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatar(r) {
|
|
136
|
+
const rel = (a) => barra(path.relative(r.raiz, a));
|
|
137
|
+
const linhas = [`## Conferência de fontes — ${r.crew}`, ''];
|
|
138
|
+
for (const i of r.refs.filter((x) => x.estado !== 'ok')) {
|
|
139
|
+
const onde = `citado em ${i.citadoEm.map(rel).join(', ')}`;
|
|
140
|
+
if (i.estado === 'nao-portatil') {
|
|
141
|
+
linhas.push(`- ⚠️ \`${i.ref}\` é um caminho absoluto (não é portátil — quebra em outro computador).${i.sugestao ? ` Sugestão: \`${i.sugestao}\`` : ''} (${onde})`);
|
|
142
|
+
} else if (i.sugestao) {
|
|
143
|
+
linhas.push(`- ❌ Não encontrei \`${i.ref}\` (${onde}). Novo caminho sugerido: \`${i.sugestao}\``);
|
|
144
|
+
} else if (i.candidatos.length) {
|
|
145
|
+
linhas.push(`- ❌ Não encontrei \`${i.ref}\` (${onde}). Encontrei ${i.candidatos.length} candidatos: ${i.candidatos.map((c) => `\`${c}\``).join(', ')}`);
|
|
146
|
+
} else if (i.pasta.length) {
|
|
147
|
+
linhas.push(`- ❌ Não encontrei \`${i.ref}\` (${onde}). Na pasta esperada existem: ${i.pasta.join(', ')}`);
|
|
148
|
+
} else {
|
|
149
|
+
linhas.push(`- ❌ Não encontrei \`${i.ref}\` (${onde}) nem nada com esse nome no projeto.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const ok = r.refs.filter((i) => i.estado === 'ok').length;
|
|
153
|
+
const pend = r.refs.filter((i) => i.estado === 'faltando').length;
|
|
154
|
+
const alertas = r.refs.filter((i) => i.estado === 'nao-portatil').length;
|
|
155
|
+
linhas.push('', `**Resumo: ${r.refs.length} fontes — ${ok} ok, ${pend} pendentes, ${alertas} alertas**`, '');
|
|
156
|
+
return linhas.join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @returns {Promise<number>} 0 = conferiu · 1 = erro de uso */
|
|
160
|
+
export async function main(argv, { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = {}) {
|
|
161
|
+
const i = argv.indexOf('--crew');
|
|
162
|
+
const crew = i > -1 ? argv[i + 1] : null;
|
|
163
|
+
if (!crew) {
|
|
164
|
+
escrever('Uso: node _opencrew/core/scripts/conferir-fontes.mjs --crew crews/<nome> [--corrigir]');
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
if (!existsSync(path.join(cwd, crew))) {
|
|
168
|
+
escrever(`Crew não encontrada: ${crew}`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
let r = await conferir({ raiz: cwd, crew });
|
|
172
|
+
escrever(formatar(r));
|
|
173
|
+
if (argv.includes('--corrigir')) {
|
|
174
|
+
const n = await corrigir({ resultado: r });
|
|
175
|
+
if (!n) escrever('Nada a corrigir.');
|
|
176
|
+
else {
|
|
177
|
+
escrever(`${n} ${n === 1 ? 'caminho corrigido' : 'caminhos corrigidos'} (cópia .bak ao lado de cada arquivo alterado).\n`);
|
|
178
|
+
r = await conferir({ raiz: cwd, crew });
|
|
179
|
+
escrever(formatar(r));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
escrever(`FONTES:${r.status}`);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
187
|
+
if (isMain) {
|
|
188
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
189
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Leitura para o verificador automático: frontmatter, limites dos formatos (constraints:),
|
|
2
|
+
// seções de um texto e proibições da memória da crew. Node puro, sem dependências.
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
function valorYaml(bruto) {
|
|
8
|
+
const v = bruto.trim();
|
|
9
|
+
const aspas = v.match(/^"(.*)"$/) ?? v.match(/^'(.*)'$/);
|
|
10
|
+
if (aspas) return aspas[1].replace(/\\"/g, '"');
|
|
11
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Frontmatter YAML simples: `chave: valor` e um nível de bloco (ex.: `constraints:`). */
|
|
16
|
+
export function lerFrontmatter(texto) {
|
|
17
|
+
const m = texto.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (!m) return null;
|
|
19
|
+
const dados = {};
|
|
20
|
+
let bloco = null;
|
|
21
|
+
for (const linha of m[1].split(/\r?\n/)) {
|
|
22
|
+
const topo = linha.match(/^([\w-]+):\s*(.*)$/);
|
|
23
|
+
if (topo) {
|
|
24
|
+
const [, chave, valor] = topo;
|
|
25
|
+
bloco = valor.trim() === '' ? chave : null;
|
|
26
|
+
dados[chave] = bloco ? {} : valorYaml(valor);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const filho = linha.match(/^\s+([\w-]+):\s*(.*)$/);
|
|
30
|
+
if (filho && bloco) dados[bloco][filho[1]] = valorYaml(filho[2]);
|
|
31
|
+
}
|
|
32
|
+
return dados;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Corpo do texto sem o frontmatter. */
|
|
36
|
+
export function semFrontmatter(texto) {
|
|
37
|
+
return texto.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Limites do formato: `constraints:` do best-practice — primeiro o do usuário
|
|
42
|
+
* (`_opencrew/best-practices.local/<id>.md`, nunca tocado pelo update), depois o do core.
|
|
43
|
+
*/
|
|
44
|
+
export async function lerLimites(raiz, formatoId) {
|
|
45
|
+
for (const pasta of [['best-practices.local'], ['core', 'best-practices']]) {
|
|
46
|
+
const arquivo = path.join(raiz, '_opencrew', ...pasta, `${formatoId}.md`);
|
|
47
|
+
if (existsSync(arquivo)) return lerFrontmatter(await readFile(arquivo, 'utf8'))?.constraints ?? {};
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Seções por cabeçalho markdown. O corpo vai até o próximo cabeçalho de nível igual ou
|
|
54
|
+
* maior, ou até uma linha `---` (separador).
|
|
55
|
+
*/
|
|
56
|
+
export function lerSecoes(texto) {
|
|
57
|
+
const linhas = semFrontmatter(texto).split(/\r?\n/);
|
|
58
|
+
const secoes = [];
|
|
59
|
+
linhas.forEach((linha, i) => {
|
|
60
|
+
const h = linha.match(/^(#{1,6})\s+(.*)$/);
|
|
61
|
+
if (!h) return;
|
|
62
|
+
const nivel = h[1].length;
|
|
63
|
+
const corpo = [];
|
|
64
|
+
for (const seguinte of linhas.slice(i + 1)) {
|
|
65
|
+
const h2 = seguinte.match(/^(#{1,6})\s/);
|
|
66
|
+
if ((h2 && h2[1].length <= nivel) || /^---\s*$/.test(seguinte)) break;
|
|
67
|
+
corpo.push(seguinte);
|
|
68
|
+
}
|
|
69
|
+
secoes.push({ titulo: h[2].trim(), nivel, corpo: corpo.join('\n').trim() });
|
|
70
|
+
});
|
|
71
|
+
return secoes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const semAcento = (s) => s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
|
|
75
|
+
export { semAcento };
|
|
76
|
+
|
|
77
|
+
const ASPAS = /"([^"]+)"|“([^”]+)”|‘([^’]+)’|`([^`]+)`|(?<![\p{L}])'([^']+)'(?![\p{L}])/gu;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Termos proibidos: o que estiver entre aspas nos itens de `## Proibições Explícitas`
|
|
81
|
+
* do `memories.md` da crew. Itens sem aspas são contados, mas não verificados.
|
|
82
|
+
*/
|
|
83
|
+
export async function lerProibicoes(raiz, crew) {
|
|
84
|
+
const arquivo = path.join(raiz, crew, '_memory', 'memories.md');
|
|
85
|
+
if (!existsSync(arquivo)) return { existe: false, termos: [], semAspas: 0 };
|
|
86
|
+
const secao = lerSecoes(await readFile(arquivo, 'utf8'))
|
|
87
|
+
.find((s) => s.nivel === 2 && semAcento(s.titulo).startsWith('proibicoes explicitas'));
|
|
88
|
+
const termos = [];
|
|
89
|
+
let semAspas = 0;
|
|
90
|
+
for (const linha of (secao?.corpo ?? '').split('\n').filter((l) => /^\s*[-*]\s+/.test(l))) {
|
|
91
|
+
const achados = [...linha.matchAll(ASPAS)].map((m) => m.slice(1).find(Boolean));
|
|
92
|
+
if (achados.length) termos.push(...achados);
|
|
93
|
+
else semAspas += 1;
|
|
94
|
+
}
|
|
95
|
+
return { existe: true, termos, semAspas };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Domínio do site da empresa (para separar links internos de externos), se houver. */
|
|
99
|
+
export async function lerDominioDoSite(raiz) {
|
|
100
|
+
const arquivo = path.join(raiz, '_opencrew', '_memory', 'company.md');
|
|
101
|
+
if (!existsSync(arquivo)) return null;
|
|
102
|
+
const m = (await readFile(arquivo, 'utf8')).match(/(?:site|website)[^\n]*?https?:\/\/([^\s/)>\]]+)/i);
|
|
103
|
+
return m ? m[1].replace(/^www\./, '').toLowerCase() : null;
|
|
104
|
+
}
|