@aksp/opencrew 1.7.0 → 1.7.1
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 +35 -0
- package/package.json +1 -1
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/architect.agent.yaml +1 -1
- package/templates/_opencrew/core/prompts/discovery.prompt.md +1 -1
- package/templates/_opencrew/core/runner.pipeline.md +78 -94
- package/templates/_opencrew/core/scripts/caminho/argumentos.mjs +49 -0
- package/templates/_opencrew/core/scripts/caminho/disco.mjs +36 -0
- package/templates/_opencrew/core/scripts/caminho/nucleo.mjs +61 -0
- package/templates/_opencrew/core/scripts/caminho.mjs +124 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,41 @@
|
|
|
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.7.1] — 2026-10-06
|
|
7
|
+
|
|
8
|
+
Fase R3 "Reparos do runner em uso real" (`specs/fase-r3-runner-em-uso-real.md`): dois defeitos
|
|
9
|
+
achados numa execução real de crew, seguindo o runner ao pé da letra. Chega a quem já usa com um
|
|
10
|
+
`npx @aksp/opencrew@latest update`. Execuções antigas continuam legíveis.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **A crew parava no segundo passo com "Input … not found".** O passo procurava o arquivo de
|
|
14
|
+
entrada num caminho em que o passo anterior não tinha gravado (a pesquisa estava em `v1/`, e a
|
|
15
|
+
entrada era procurada fora dela). Agora a entrada de um passo é sempre a saída mais nova daquele
|
|
16
|
+
arquivo, em qualquer pasta de versão.
|
|
17
|
+
- **No Windows, cada conferência dependia de a IA traduzir um comando de bash** (`test -s`,
|
|
18
|
+
`grep`, `ls | sort | tail`, `mkdir -p`). Um erro de tradução virava validação que falhava sem
|
|
19
|
+
motivo. Esses comandos saíram do runner.
|
|
20
|
+
- O Architect proibia criar pasta por comando e o runner mandava criar: os dois agora dizem a
|
|
21
|
+
mesma coisa (ninguém cria pasta por comando).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Quem calcula os caminhos da execução é um script, não a IA.** O runner roda um comando curto
|
|
25
|
+
(`_opencrew/core/scripts/caminho.mjs`), igual em qualquer sistema, para criar a pasta da
|
|
26
|
+
execução, saber onde cada passo grava, achar a entrada e conferir o arquivo gravado. Existência,
|
|
27
|
+
número de seções e TL;DR saem numa conferência só; antes eram até três.
|
|
28
|
+
- A pasta de versão continua subindo como antes (`v1`, `v2`, `v3`… a cada passo que grava), agora
|
|
29
|
+
em ordem numérica (`v10` vem depois de `v9`). O script só cria pastas, e só dentro de
|
|
30
|
+
`crews/<crew>/output/<execução>/`; nunca cria, altera nem apaga arquivo.
|
|
31
|
+
- Se o script não rodar (sem Node, por exemplo), a execução não para: o runner avisa uma vez,
|
|
32
|
+
segue pela regra escrita e lista esses arquivos como "não verificado" na aprovação final.
|
|
33
|
+
- Saber se a memória da crew está no formato novo e se o `runs.md` existe passa a ser feito lendo
|
|
34
|
+
o arquivo, sem comando de terminal. Na criação de crew, as crews existentes são listadas pela
|
|
35
|
+
ferramenta da IDE, não por `ls`.
|
|
36
|
+
|
|
37
|
+
### Internal
|
|
38
|
+
- Travas novas: `tests/caminho.test.js`, `tests/caminho-casca.test.js`,
|
|
39
|
+
`tests/runtime-contracts-r3.test.js` e `tests/upgrade-r3.test.js`; os testes R2-04d que contavam
|
|
40
|
+
comandos de bash no runner passam a proteger as aspas nos comandos novos.
|
|
6
41
|
## [1.7.0] — 2026-10-06
|
|
7
42
|
|
|
8
43
|
Fase E1 "Escritório ao vivo — a equipe trabalhando, em 8 bits"
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.7.
|
|
1
|
+
1.7.1
|
|
@@ -32,7 +32,7 @@ agent:
|
|
|
32
32
|
- Each agent must have exactly one clear responsibility
|
|
33
33
|
- Pipelines must have checkpoints at every user decision point
|
|
34
34
|
- Default to the simplest pipeline that achieves the goal
|
|
35
|
-
- "Path safety: Never
|
|
35
|
+
- "Path safety: Never create directories by command (no Bash mkdir, on any system). Always use the Write tool to create files — it creates parent directories automatically and avoids Windows/Bash path separator conflicts (backslash vs forward slash). At run time, the folders of a run are created by the runner (`_opencrew/core/scripts/caminho.mjs`), never by a command of yours."
|
|
36
36
|
|
|
37
37
|
discussion: true
|
|
38
38
|
|
|
@@ -305,7 +305,7 @@ target_formats: # content crews only; empty list for others
|
|
|
305
305
|
|
|
306
306
|
The `crew_code` must be a short, URL-safe slug derived from the crew's purpose (e.g., `content-calendar`, `competitor-tracker`, `lead-notify`).
|
|
307
307
|
|
|
308
|
-
**CRITICAL — Name uniqueness:** The `crew_code` MUST NEVER match any existing folder name in `crews/`. Before finalizing,
|
|
308
|
+
**CRITICAL — Name uniqueness:** The `crew_code` MUST NEVER match any existing folder name in `crews/`. Before finalizing, list the existing folders of `crews/` with the IDE's folder-listing tool (no shell command); if `crews/` does not exist yet, there are none. If the slug you derive matches an existing folder, append a numeric suffix (`-2`, `-3`, etc.) until it is unique. Never reuse an existing crew folder name — doing so would overwrite another crew's files.
|
|
309
309
|
|
|
310
310
|
---
|
|
311
311
|
|
|
@@ -53,12 +53,9 @@ Before starting execution:
|
|
|
53
53
|
> unless the user base expands beyond PT-BR — at that point, discuss a migration strategy
|
|
54
54
|
> (e.g. i18n key mapping) rather than mixing languages in a single file.
|
|
55
55
|
|
|
56
|
-
1b. **Memory format migration** — After loading `memories.md`, check whether it uses the new format
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
- If `NEW_FORMAT` → proceed normally.
|
|
61
|
-
- If `OLD_FORMAT` (or file is empty / does not exist) → migrate before proceeding:
|
|
56
|
+
1b. **Memory format migration** — After loading `memories.md`, check whether it uses the new format: it does when it has the `## Estilo de Escrita` section header (read the file with the read tool — no command).
|
|
57
|
+
- If it has the header → proceed normally.
|
|
58
|
+
- If it does not (or the file is empty / does not exist) → migrate before proceeding:
|
|
62
59
|
a0. If the file exists and is not empty, FIRST copy it to `crews/{name}/_memory/memories.md.bak`
|
|
63
60
|
(never lose what the crew learned), then tell the user in one line:
|
|
64
61
|
"Atualizei o formato da memória da crew; a versão anterior está em `memories.md.bak`."
|
|
@@ -78,11 +75,8 @@ Before starting execution:
|
|
|
78
75
|
## Técnico (específico do crew)
|
|
79
76
|
```
|
|
80
77
|
(Use the crew's display name for `{crew-name}`, and the crew code for `{name}` in file paths — they refer to the same crew.)
|
|
81
|
-
b. Check if `crews/{name}/_memory/runs.md` exists
|
|
82
|
-
|
|
83
|
-
test -f "crews/{name}/_memory/runs.md" && echo "EXISTS" || echo "MISSING"
|
|
84
|
-
```
|
|
85
|
-
If `MISSING`, create it with:
|
|
78
|
+
b. Check if `crews/{name}/_memory/runs.md` exists (read tool — no command).
|
|
79
|
+
If it does not exist, create it with:
|
|
86
80
|
```markdown
|
|
87
81
|
# Run History: {crew-name}
|
|
88
82
|
|
|
@@ -229,9 +223,9 @@ Before starting execution:
|
|
|
229
223
|
identical to today: all agents listed, no Skipped line.
|
|
230
224
|
5b. **Initialize run folder**: Generate a unique run ID for this execution:
|
|
231
225
|
- Format: `YYYY-MM-DD-HHmmss` using the current timestamp (e.g. `2026-03-03-143022`)
|
|
232
|
-
- Check if `crews/{name}/output/{run_id}/` already exists
|
|
226
|
+
- Check (folder-listing tool, no command) if `crews/{name}/output/{run_id}/` already exists
|
|
233
227
|
- If it does (sub-second collision), append `-2`, `-3`, etc. until the folder does not exist
|
|
234
|
-
- Create the folder
|
|
228
|
+
- Create the folder: run the `pasta` command (see "Output Path Transformation" below) — never create a folder by command yourself
|
|
235
229
|
- Store `run_id` in working memory for this run — it will be used for ALL output paths
|
|
236
230
|
6. **Escritório** — if it is on, run `iniciar`, then one `pular` per deselected agent, one after the other (see "Escritório" below).
|
|
237
231
|
|
|
@@ -379,11 +373,9 @@ Before executing any step that references an agent:
|
|
|
379
373
|
To prevent linear token growth across multi-agent pipelines, apply context compression
|
|
380
374
|
when passing prior agents' outputs as context:
|
|
381
375
|
|
|
382
|
-
1. **TL;DR extraction**: After each agent completes, check if its output contains a `## TL;DR` section
|
|
376
|
+
1. **TL;DR extraction**: After each agent completes, check if its output contains a `## TL;DR` section
|
|
377
|
+
(a line starting with `## TL;DR` — you have the output, no command is needed).
|
|
383
378
|
If present, extract and store it separately as the agent's summary.
|
|
384
|
-
```bash
|
|
385
|
-
grep -q "^## TL;DR" "{outputFile}" && echo "HAS_TLDR" || echo "NO_TLDR"
|
|
386
|
-
```
|
|
387
379
|
|
|
388
380
|
2. **Compressed context assembly**: When preparing context for Agent N:
|
|
389
381
|
- Include **TL;DR summaries** from Agents 1 through N-2 (all agents except the direct predecessor)
|
|
@@ -441,7 +433,7 @@ When an agent's `.agent.md` frontmatter contains a `tasks:` field:
|
|
|
441
433
|
e. Check task veto conditions (same enforcement as step veto conditions below)
|
|
442
434
|
|
|
443
435
|
3. **Final output**: The output of the LAST task in the chain becomes the step's output
|
|
444
|
-
-
|
|
436
|
+
- Resolve the `outputFile` path with the `saida` command (Output Path Transformation) before saving — this applies regardless of whether the step runs as `execution: inline` or `execution: subagent`
|
|
445
437
|
- Save to the **transformed** outputFile path
|
|
446
438
|
- This is what the next step (or checkpoint) receives
|
|
447
439
|
|
|
@@ -455,40 +447,44 @@ When an agent's `.agent.md` frontmatter contains a `tasks:` field:
|
|
|
455
447
|
|
|
456
448
|
### Output Path Transformation
|
|
457
449
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
- If the path starts with `crews/{name}/output/`, insert `{run_id}/` immediately after `output/`
|
|
463
|
-
- Example: `crews/carousel/output/slides/draft.md` → `crews/carousel/output/2026-03-03-143022/slides/draft.md`
|
|
464
|
-
- Example: `crews/carousel/output/angles-brief.yaml` → `crews/carousel/output/2026-03-03-143022/angles-brief.yaml`
|
|
465
|
-
- If the path does NOT start with `crews/{name}/output/`, leave it unchanged
|
|
466
|
-
|
|
467
|
-
#### Step 2 — Insert version folder
|
|
468
|
-
|
|
469
|
-
Apply to every path that was transformed in Step 1:
|
|
470
|
-
|
|
471
|
-
1. Determine the **output group** = the parent directory of the file (after Step 1 transformation)
|
|
472
|
-
- Example: `crews/carousel/output/2026-03-03-143022/slides/draft.md` → group is `crews/carousel/output/2026-03-03-143022/slides/`
|
|
473
|
-
- Example: `crews/carousel/output/2026-03-03-143022/angles-brief.yaml` → group is `crews/carousel/output/2026-03-03-143022/`
|
|
474
|
-
|
|
475
|
-
2. Detect existing versions for this group using Bash:
|
|
476
|
-
```bash
|
|
477
|
-
ls -1 "crews/{name}/output/{run_id}/{relative-group}/" 2>/dev/null | grep -E '^v[0-9]+$' | sort -V | tail -1
|
|
478
|
-
```
|
|
479
|
-
- If the command returns a version (e.g. `v2`) → use `v3`
|
|
480
|
-
(Always increment the highest version found, even if lower versions have gaps — e.g. if `v1` and `v3` exist, use `v4`)
|
|
481
|
-
- If the command returns nothing (no versions yet) → use `v1`
|
|
482
|
-
(`{relative-group}` is the portion of the group path after `crews/{name}/output/{run_id}/`, e.g. `slides/` or empty string for root-level files)
|
|
483
|
-
|
|
484
|
-
3. Insert the version folder immediately before the filename:
|
|
485
|
-
- `crews/carousel/output/2026-03-03-143022/slides/draft.md` → `crews/carousel/output/2026-03-03-143022/slides/v1/draft.md`
|
|
486
|
-
- `crews/carousel/output/2026-03-03-143022/angles-brief.yaml` → `crews/carousel/output/2026-03-03-143022/v1/angles-brief.yaml`
|
|
487
|
-
|
|
488
|
-
4. **Cache per group**: within a single step execution, once a version is determined for a group, reuse it for all subsequent files in that same group. Do not re-run the `ls` per file.
|
|
489
|
-
If the same file path is written twice within a step, both writes go to the same versioned path (the second write overwrites the first within that version).
|
|
450
|
+
The path of every file of the run comes from one script (`caminho.mjs`), the same on every system —
|
|
451
|
+
never from a path you put together, never from a shell command of your own. Run from the project
|
|
452
|
+
root the one-line command of each moment and read the last line (`CAMINHO:OK {path}`,
|
|
453
|
+
`CAMINHO:FALTA {path}` or `CAMINHO:REPROVADO {motivo}`):
|
|
490
454
|
|
|
491
|
-
|
|
455
|
+
| Moment | Command |
|
|
456
|
+
|---|---|
|
|
457
|
+
| Start of the run (Initialization, step 5b) | `node _opencrew/core/scripts/caminho.mjs "{name}" pasta --run "{run_id}"` |
|
|
458
|
+
| Before a step, for its `inputFile` | `node _opencrew/core/scripts/caminho.mjs "{name}" entrada --run "{run_id}" --arquivo "{inputFile}"` |
|
|
459
|
+
| Before a step writes, for the first `outputFile` of each group | `node _opencrew/core/scripts/caminho.mjs "{name}" saida --run "{run_id}" --arquivo "{outputFile}"` |
|
|
460
|
+
| After a step wrote, for each output file | `node _opencrew/core/scripts/caminho.mjs "{name}" conferir --arquivo "{path}"` |
|
|
461
|
+
|
|
462
|
+
- **Values** — `{name}`: the crew code. `{inputFile}` / `{outputFile}`: the path as the step
|
|
463
|
+
declares it (raw, without the run_id). `{path}`: the path `saida` returned. The safe-name rule
|
|
464
|
+
(nome seguro) applies: the crew and every path between double quotes.
|
|
465
|
+
- **`saida`** answers with the **transformed** path and creates its folder: write the file there,
|
|
466
|
+
never to the raw path. Run it once per group in a step — the other `outputFile`s of the same
|
|
467
|
+
group reuse the version folder it returned, and a file written twice in a step goes to the same path.
|
|
468
|
+
- **`entrada`** answers with the newest output of that file: use the path it returns, whatever
|
|
469
|
+
its version folder.
|
|
470
|
+
- **The rule the script applies** (apply it yourself only when the script does not run):
|
|
471
|
+
1. A declared path that starts with `crews/{name}/output/` gets `{run_id}/` right after
|
|
472
|
+
`output/`; any other path stays as declared, with no version folder.
|
|
473
|
+
2. The **group** is the folder of the file, run_id included (`…/output/{run_id}/`, or
|
|
474
|
+
`…/output/{run_id}/slides/`). A step writes to the group's next version folder: the highest
|
|
475
|
+
`vN` there plus 1, or `v1` when there is none — numeric order (`v10` comes after `v9`), gaps
|
|
476
|
+
not filled (`v1` and `v3` → `v4`).
|
|
477
|
+
3. A step reads the newest version that has the file: from the highest `vN` down, the first
|
|
478
|
+
where the file exists and is not empty; then the group itself, with no version folder (where
|
|
479
|
+
checkpoint answers live).
|
|
480
|
+
|
|
481
|
+
Example, one group: the researcher writes `…/v1/pesquisa.md`, the writer writes `…/v2/post.md`
|
|
482
|
+
and reads `…/v1/pesquisa.md`. Never assume `v1`.
|
|
483
|
+
- **Script that does not run** (no Node, an error, or no `CAMINHO:` line): tell the user once per
|
|
484
|
+
run `Não consegui rodar a conferência de caminhos; sigo pela regra escrita e marco os arquivos como não verificados.`,
|
|
485
|
+
build the path by the rule above (the Write tool creates the folder) and continue. A file handled
|
|
486
|
+
this way skips its gate and is listed at the final approval:
|
|
487
|
+
`{arquivo} — não verificado: a conferência de caminhos não rodou`.
|
|
492
488
|
|
|
493
489
|
### For each pipeline step:
|
|
494
490
|
|
|
@@ -505,13 +501,9 @@ Apply this transformation consistently for every write in this step.
|
|
|
505
501
|
|
|
506
502
|
0b. **Escritório** — if it is on, run `passo`, or `checkpoint` when the step is a checkpoint (see "Escritório" above).
|
|
507
503
|
|
|
508
|
-
1. **Pre-Step Input Validation** — MANDATORY. If the step's frontmatter declares an `inputFile`, validate that the input exists before executing the step. Run
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
```
|
|
512
|
-
- Apply the Output Path Transformation (Step 1: run_id injection) to the `inputFile` path before running the check.
|
|
513
|
-
- If the Bash output contains `VALIDATION:PASS` → proceed to execute the step.
|
|
514
|
-
- If the Bash output contains `VALIDATION:FAIL` → do NOT execute the step. Present to user:
|
|
504
|
+
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
|
+
- `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.
|
|
506
|
+
- `CAMINHO:FALTA {path}` → do NOT execute the step. Present to user:
|
|
515
507
|
```
|
|
516
508
|
⚠️ Input for {Agent Name} not found: {path}
|
|
517
509
|
The previous step may have failed to produce output.
|
|
@@ -530,7 +522,7 @@ Apply this transformation consistently for every write in this step.
|
|
|
530
522
|
- Inform user: `🔍 {Agent Name} is working in the background...`
|
|
531
523
|
- Read the step's `model_tier` frontmatter field (if present).
|
|
532
524
|
Valid values: `fast` or `powerful`. If absent or any other value: default to `powerful`.
|
|
533
|
-
- **Before building the subagent prompt**:
|
|
525
|
+
- **Before building the subagent prompt**: Resolve all output paths referenced in the step file with the `saida` command (Output Path Transformation, once per group). Store the transformed path(s) in working memory — they will be used both in the prompt and in post-completion verification. Never pass raw paths from the step file to the subagent.
|
|
534
526
|
- Use the Task tool to dispatch the step as a subagent:
|
|
535
527
|
- If `model_tier: fast`: use the fastest/lightest model available in your current IDE.
|
|
536
528
|
- If `model_tier: powerful` or absent/invalid: use the default model (no model override needed)
|
|
@@ -542,7 +534,7 @@ Apply this transformation consistently for every write in this step.
|
|
|
542
534
|
- The veto conditions from the step file (agent should self-check before completing)
|
|
543
535
|
- The company context
|
|
544
536
|
- The crew memory
|
|
545
|
-
- The **transformed** path to save output (e.g
|
|
537
|
+
- The **transformed** path to save output (the one `saida` returned, e.g. `crews/{name}/output/2026-03-20-140736/slides/v2/draft.md`)
|
|
546
538
|
- Wait for the subagent to complete
|
|
547
539
|
- Inform user: `✓ {Agent Name} completed`
|
|
548
540
|
- Proceed to Post-Step Output Validation (below) before advancing.
|
|
@@ -552,13 +544,13 @@ Apply this transformation consistently for every write in this step.
|
|
|
552
544
|
- Announce: `{icon} {Agent Name} is working...`
|
|
553
545
|
- Follow the step instructions
|
|
554
546
|
- Present output directly in the conversation
|
|
555
|
-
- Save output to the specified output file —
|
|
547
|
+
- Save output to the specified output file — resolve the path with the `saida` command (Output Path Transformation) before writing. Do not write to the raw path from the step file.
|
|
556
548
|
- Proceed to Post-Step Output Validation (below) before advancing.
|
|
557
549
|
|
|
558
550
|
#### If `type: checkpoint`
|
|
559
551
|
- Present the checkpoint message to the user
|
|
560
552
|
- If the checkpoint requires a choice (numbered list), present options as a numbered list
|
|
561
|
-
- **Always include the file path** of any generated content the user needs to review. Example: "Review the content at `crews/{name}/output/{run_id}/
|
|
553
|
+
- **Always include the file path** of any generated content the user needs to review. Example: "Review the content at `crews/{name}/output/{run_id}/v2/content.md` and let me know if it looks good." (the path the script returned)
|
|
562
554
|
- Wait for user input before proceeding
|
|
563
555
|
- Save the user's choice/response for the next step
|
|
564
556
|
- **Correction → memory, right away**: if the answer corrects something (tone, audience, a term,
|
|
@@ -571,7 +563,7 @@ Apply this transformation consistently for every write in this step.
|
|
|
571
563
|
(e.g. the organization's name, the main audience), ask: "Isso vale para todas as crews?
|
|
572
564
|
Atualizo o perfil da empresa?" — change `company.md` only after a yes.
|
|
573
565
|
- **If the step frontmatter contains `outputFile`**: after collecting the user's full response,
|
|
574
|
-
|
|
566
|
+
insert only the run_id in the `outputFile` path (item 1 of the rule in Output Path Transformation — no version folder, no `saida` command), then write the response to that path using the Write tool (it creates the folder) before moving to the next step. Checkpoint files are user input captures, not versioned output: they live in the group itself, where `entrada` finds them.
|
|
575
567
|
Use this format:
|
|
576
568
|
```
|
|
577
569
|
# Research Focus
|
|
@@ -584,28 +576,22 @@ Apply this transformation consistently for every write in this step.
|
|
|
584
576
|
|
|
585
577
|
### Post-Step Output Validation
|
|
586
578
|
|
|
587
|
-
After a step produces output (subagent or inline) and BEFORE Veto Condition Enforcement, the runner MUST validate that the declared output files exist and are non-empty. This is a binary, non-negotiable gate — the runner does NOT proceed on memory or assumption, only on
|
|
579
|
+
After a step produces output (subagent or inline) and BEFORE Veto Condition Enforcement, the runner MUST validate that the declared output files exist and are non-empty. This is a binary, non-negotiable gate — the runner does NOT proceed on memory or assumption, only on the script's `CAMINHO:` line.
|
|
588
580
|
|
|
589
|
-
**If the step declares an `outputFile`** (single or multiple), run
|
|
581
|
+
**If the step declares an `outputFile`** (single or multiple), run the `conferir` command (Output Path Transformation) for EACH output file, with the **stored transformed path** (the one `saida` returned), not the raw path from the step file. A step with an `output_contract:` adds its options to this same call (see Output Contract Validation): one command per file.
|
|
590
582
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
Use the **stored transformed path** (after Output Path Transformation Steps 1 and 2), not the raw path from the step file.
|
|
596
|
-
|
|
597
|
-
**Rules:**
|
|
598
|
-
- If ALL output files return `VALIDATION:PASS` → proceed to Veto Condition Enforcement.
|
|
583
|
+
**Rules** (`FAIL` below = the last line is `CAMINHO:REPROVADO arquivo ausente ou vazio`):
|
|
584
|
+
- If ALL output files return `CAMINHO:OK` → proceed to Veto Condition Enforcement.
|
|
599
585
|
- **Irreversible step** (`side_effects: irreversible` — publish, post, send) with ANY
|
|
600
|
-
`
|
|
586
|
+
`FAIL` → NEVER re-execute it. Tell the user: "⚠️ {Agent Name} did not save its
|
|
601
587
|
output, but the action may already have happened (post published / email sent). Check
|
|
602
588
|
before retrying." Then offer: 1. Retry step (only after the user checked) · 2. Mark as done
|
|
603
589
|
and continue · 3. Abort pipeline.
|
|
604
|
-
- If ANY output file returns `
|
|
590
|
+
- If ANY output file returns `FAIL` (any other step):
|
|
605
591
|
1. **Retry once**: re-execute the entire step with the same input and context.
|
|
606
592
|
2. After re-execution, run the validation again for all output files.
|
|
607
|
-
3. If second attempt returns `
|
|
608
|
-
4. If second attempt still has ANY `
|
|
593
|
+
3. If second attempt returns `CAMINHO:OK` for all files → proceed normally.
|
|
594
|
+
4. If second attempt still has ANY `FAIL` → present to user:
|
|
609
595
|
```
|
|
610
596
|
⚠️ {Agent Name}'s output was not generated: {path}
|
|
611
597
|
|
|
@@ -617,26 +603,23 @@ Use the **stored transformed path** (after Output Path Transformation Steps 1 an
|
|
|
617
603
|
- If the step does not declare an `outputFile` (e.g., steps that only produce inline console output) → skip output validation.
|
|
618
604
|
- Checkpoint steps (`type: checkpoint`) are exempt — their output is the user's response, not a file.
|
|
619
605
|
|
|
620
|
-
**IMPORTANT**: Do NOT rely on reading the file with the Read tool to "verify" output. The Read tool returns content that can be misinterpreted. Use ONLY the
|
|
606
|
+
**IMPORTANT**: Do NOT rely on reading the file with the Read tool to "verify" output. The Read tool returns content that can be misinterpreted. Use ONLY the `conferir` command — its last line is binary and cannot be hallucinated.
|
|
621
607
|
|
|
622
608
|
### Output Contract Validation
|
|
623
609
|
|
|
624
610
|
If the step's frontmatter declares an `output_contract:` field, apply structured validation
|
|
625
|
-
|
|
611
|
+
in the same call as the basic file existence check (Post-Step Output Validation):
|
|
626
612
|
|
|
627
|
-
1. **Required sections check**: If `output_contract.required_sections` is defined,
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
grep -c "^## " "{transformed outputFile path}" | xargs -I {} test {} -ge {min_sections} && echo "SECTIONS:PASS" || echo "SECTIONS:FAIL"
|
|
631
|
-
```
|
|
613
|
+
1. **Required sections check**: If `output_contract.required_sections` is defined, add
|
|
614
|
+
`--secoes {min_sections}` to the same `conferir` command: the file needs at least that many
|
|
615
|
+
lines starting with `## `.
|
|
632
616
|
|
|
633
|
-
2. **TL;DR check**: If the output contract requires a TL;DR section
|
|
634
|
-
|
|
635
|
-
grep -q "^## TL;DR" "{transformed outputFile path}" && echo "TLDR:PASS" || echo "TLDR:FAIL"
|
|
636
|
-
```
|
|
617
|
+
2. **TL;DR check**: If the output contract requires a TL;DR section, add `--tldr` to the same
|
|
618
|
+
`conferir` command.
|
|
637
619
|
|
|
638
|
-
3. **If
|
|
639
|
-
|
|
620
|
+
3. **If a check fails** (the last line is `CAMINHO:REPROVADO {motivo}`, with a motivo other than
|
|
621
|
+
`arquivo ausente ou vazio`; the script reports the first one):
|
|
622
|
+
- Present to user: "⚠️ Output from {Agent Name} is incomplete: {motivo}"
|
|
640
623
|
- Options as numbered list:
|
|
641
624
|
1. Accept anyway and continue
|
|
642
625
|
2. Retry step (re-execute the agent)
|
|
@@ -720,7 +703,8 @@ When a step has `on_reject: {step-id}` (a review step):
|
|
|
720
703
|
lines under each file, not the "não é texto" line of **Notas**), one per line as
|
|
721
704
|
`{arquivo} — {motivo}`, then the lines under `**Notas:**` in that report, as they are written,
|
|
722
705
|
and repeat every "não rodou" warning of this run (checker and source check) and the line of
|
|
723
|
-
every file left unchecked by the safe-name rule.
|
|
706
|
+
every file left unchecked by the safe-name rule. List the same way every file the path script
|
|
707
|
+
did not check (see Output Path Transformation). If the approved
|
|
724
708
|
text still contains `[PREENCHER: …]`, ask the user for each missing piece of real information
|
|
725
709
|
and write it into the text before approving.
|
|
726
710
|
|
|
@@ -731,14 +715,14 @@ For reference, the complete execution order for each pipeline step is:
|
|
|
731
715
|
```
|
|
732
716
|
0. Agent deselection check (skip step if its agent was deselected)
|
|
733
717
|
0b. Escritório command (passo or checkpoint) — only if it is on
|
|
734
|
-
1. Pre-Step Input Validation (
|
|
718
|
+
1. Pre-Step Input Validation (script gate: `entrada`)
|
|
735
719
|
2. Read step file
|
|
736
720
|
3. Check execution mode and execute (subagent / inline / checkpoint)
|
|
737
|
-
4. Post-Step Output Validation (
|
|
721
|
+
4. Post-Step Output Validation (script gate: `conferir`)
|
|
738
722
|
5. Veto Condition Enforcement
|
|
739
723
|
```
|
|
740
724
|
|
|
741
|
-
Steps 1 and 4 are binary
|
|
725
|
+
Steps 1 and 4 are binary script gates. If either fails, the pipeline does NOT advance — the user is consulted.
|
|
742
726
|
|
|
743
727
|
### After Pipeline Completion
|
|
744
728
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Linha de comando do `caminho.mjs`: a crew, a ação, as opções e a linha de uso.
|
|
2
|
+
// Spec: fase-r3-runner-em-uso-real.md, §3 e §6 (repositório do OpenCrew).
|
|
3
|
+
import { MSG } from '../comum.mjs';
|
|
4
|
+
import { ACOES } from './nucleo.mjs';
|
|
5
|
+
|
|
6
|
+
export const USO = 'Uso: node _opencrew/core/scripts/caminho.mjs <crew> <ação> --run <id> [opções]';
|
|
7
|
+
|
|
8
|
+
const LISTA = ACOES.join(', ');
|
|
9
|
+
const OPCAO = /^--(run|arquivo|secoes|tldr)(?:=(.*))?$/s;
|
|
10
|
+
const RUN = /^(?!\.+$)[A-Za-z0-9._-]+$/;
|
|
11
|
+
const INTEIRO = /^[1-9]\d{0,8}$/;
|
|
12
|
+
|
|
13
|
+
/** Texto que veio da linha de comando e volta numa mensagem: uma linha só, até 200 caracteres. */
|
|
14
|
+
export const limpar = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `argv` → `{ crew, acao, run, arquivo, secoes, tldr }`. Os dois primeiros argumentos soltos são
|
|
18
|
+
* a crew e a ação. Opção vale como `--nome valor` e `--nome=valor`; `--tldr` não leva valor.
|
|
19
|
+
* Opção ausente fica `undefined`.
|
|
20
|
+
*/
|
|
21
|
+
export function lerArgs(argv) {
|
|
22
|
+
const soltos = [];
|
|
23
|
+
const opcoes = {};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const [, nome, colado] = argv[i].match(OPCAO) ?? [];
|
|
26
|
+
if (!nome) soltos.push(argv[i]);
|
|
27
|
+
else if (nome === 'tldr') opcoes.tldr = true;
|
|
28
|
+
else if (colado !== undefined) opcoes[nome] = colado;
|
|
29
|
+
else opcoes[nome] = i + 1 < argv.length && !OPCAO.test(argv[i + 1]) ? argv[++i] : '';
|
|
30
|
+
}
|
|
31
|
+
const [crew, acao] = soltos.filter((s) => !s.startsWith('--'));
|
|
32
|
+
return { crew, acao, tldr: false, ...opcoes };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* O que falta ou está errado na linha de comando, antes de olhar o disco. `conferir` recebe um
|
|
37
|
+
* caminho já resolvido: é a única ação que não precisa de `--run`.
|
|
38
|
+
* @returns {string|null} o motivo em PT-BR, ou `null`
|
|
39
|
+
*/
|
|
40
|
+
export function erroDeArgumentos({ crew, acao, run, arquivo, secoes }) {
|
|
41
|
+
if (!crew) return 'Falta o nome da crew.';
|
|
42
|
+
if (!acao) return `Falta a ação. Ações: ${LISTA}.`;
|
|
43
|
+
if (!ACOES.includes(acao)) return `Ação desconhecida: ${limpar(acao)}. Ações: ${LISTA}.`;
|
|
44
|
+
if (acao !== 'conferir' && !run) return MSG.faltaOpcao('--run');
|
|
45
|
+
if (run !== undefined && !RUN.test(run)) return 'O --run só aceita letras, dígitos, ponto, sublinhado e hífen.';
|
|
46
|
+
if (acao !== 'pasta' && !arquivo) return MSG.faltaOpcao('--arquivo');
|
|
47
|
+
if (secoes !== undefined && !INTEIRO.test(secoes)) return 'O --secoes é um número inteiro a partir de 1.';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// O que o `caminho.mjs` faz no disco: lê nomes de pastas, confere se um arquivo tem conteúdo, lê
|
|
2
|
+
// um arquivo e cria pastas. Nunca cria, altera nem apaga arquivo.
|
|
3
|
+
// Spec: fase-r3-runner-em-uso-real.md, regra 6 (repositório do OpenCrew).
|
|
4
|
+
import { mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
/** Os nomes das pastas que ficam direto em `pasta`; pasta que não existe não tem nenhuma. */
|
|
7
|
+
export function pastasDe(pasta) {
|
|
8
|
+
try {
|
|
9
|
+
return readdirSync(pasta, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ehPasta(caminho) {
|
|
16
|
+
try {
|
|
17
|
+
return statSync(caminho).isDirectory();
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** É um arquivo, existe e não está vazio? */
|
|
24
|
+
export function temConteudo(arquivo) {
|
|
25
|
+
try {
|
|
26
|
+
const info = statSync(arquivo);
|
|
27
|
+
return info.isFile() && info.size > 0;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const lerTexto = (arquivo) => readFileSync(arquivo, 'utf8');
|
|
34
|
+
|
|
35
|
+
/** Cria a pasta com as pastas-mãe; pasta que já existe não é erro. */
|
|
36
|
+
export const criarPasta = (pasta) => { mkdirSync(pasta, { recursive: true }); };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Núcleo do `caminho.mjs`: as regras de caminho de uma execução, em funções puras. Não toca em
|
|
2
|
+
// disco nem em processo (quem lê pastas e arquivos é `disco.mjs`).
|
|
3
|
+
// Spec: fase-r3-runner-em-uso-real.md, regras 2 a 5 (repositório do OpenCrew).
|
|
4
|
+
|
|
5
|
+
export const ACOES = ['pasta', 'saida', 'entrada', 'conferir'];
|
|
6
|
+
|
|
7
|
+
/** Os motivos de `CAMINHO:REPROVADO`, na ordem em que são conferidos (§6 da spec). */
|
|
8
|
+
export const MOTIVO = {
|
|
9
|
+
ausente: 'arquivo ausente ou vazio',
|
|
10
|
+
secoes: (achadas, minimo) => `${achadas} seções, mínimo ${minimo}`,
|
|
11
|
+
tldr: 'falta a seção TL;DR',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Caminho como o script o compara e devolve: com `/`, sem `./` na frente e sem barra dobrada. */
|
|
15
|
+
export const normalizar = (caminho) => String(caminho).replace(/\\/g, '/').replace(/^(?:\.\/+)+/, '').replace(/\/{2,}/g, '/');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Regra 2 — o caminho da execução. Caminho declarado que começa por `crews/<crew>/output/` ganha
|
|
19
|
+
* `<run>/` logo depois de `output/`.
|
|
20
|
+
* @returns {{ grupo: string, nome: string } | null} o grupo (a pasta do arquivo, já com o run) e
|
|
21
|
+
* o nome do arquivo; `null` quando o caminho não é de `output/` (volta como veio)
|
|
22
|
+
*/
|
|
23
|
+
export function naExecucao(declarado, crew, run) {
|
|
24
|
+
const saida = `crews/${crew}/output/`;
|
|
25
|
+
const caminho = normalizar(declarado);
|
|
26
|
+
if (!caminho.startsWith(saida)) return null;
|
|
27
|
+
const partes = caminho.slice(saida.length).split('/');
|
|
28
|
+
const nome = partes.pop();
|
|
29
|
+
return { grupo: [`${saida}${run}`, ...partes].join('/'), nome };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** O número de uma pasta de versão (`v` + número), ou `null` para qualquer outro nome. */
|
|
33
|
+
function numero(nome) {
|
|
34
|
+
const [, digitos] = /^v(\d{1,9})$/.exec(nome) ?? [];
|
|
35
|
+
return digitos === undefined ? null : Number(digitos);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** As pastas de versão, da mais nova para a mais antiga, em ordem numérica (`v10` antes de `v9`). */
|
|
39
|
+
export function daMaisNova(nomes) {
|
|
40
|
+
return nomes.filter((nome) => numero(nome) !== null).sort((a, b) => numero(b) - numero(a));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Regra 3 — a versão em que o passo grava: a maior `vN` do grupo mais 1; sem nenhuma, `v1`. */
|
|
44
|
+
export function proximaVersao(nomes) {
|
|
45
|
+
const [maior] = daMaisNova(nomes);
|
|
46
|
+
return `v${maior ? numero(maior) + 1 : 1}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Regra 5 — o que falta no texto de um arquivo gravado, pelo primeiro motivo da §6.
|
|
51
|
+
* @param {string} texto o conteúdo do arquivo (que já existe e não está vazio)
|
|
52
|
+
* @param {{ secoes?: number|null, tldr?: boolean }} pedido
|
|
53
|
+
* @returns {string|null} o motivo, ou `null` quando o arquivo passa
|
|
54
|
+
*/
|
|
55
|
+
export function motivoDeReprovacao(texto, { secoes = null, tldr = false } = {}) {
|
|
56
|
+
const linhas = (texto.charCodeAt(0) === 0xfeff ? texto.slice(1) : texto).split(/\r?\n/);
|
|
57
|
+
const achadas = linhas.filter((linha) => linha.startsWith('## ')).length;
|
|
58
|
+
if (secoes !== null && achadas < secoes) return MOTIVO.secoes(achadas, secoes);
|
|
59
|
+
if (tldr && !linhas.some((linha) => linha.startsWith('## TL;DR'))) return MOTIVO.tldr;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Caminho da execução de uma crew: diz onde cada passo grava, de onde lê e se o arquivo gravado
|
|
3
|
+
// está lá. Quem calcula é este script, igual em qualquer sistema — a IA não monta o caminho.
|
|
4
|
+
// Uso (na pasta do projeto): node _opencrew/core/scripts/caminho.mjs <crew> <ação> --run <id> [opções]
|
|
5
|
+
// pasta --run <id> cria crews/<crew>/output/<id>/
|
|
6
|
+
// saida --run <id> --arquivo <declarado> onde o passo grava (abre a pasta de versão seguinte)
|
|
7
|
+
// entrada --run <id> --arquivo <declarado> a saída mais nova desse arquivo
|
|
8
|
+
// conferir --arquivo <caminho já resolvido> [--secoes N] [--tldr]
|
|
9
|
+
// <crew> é o nome da pasta em `crews/`. <declarado> é o `inputFile` ou `outputFile` do passo.
|
|
10
|
+
// Só cria pastas, e só dentro de crews/<crew>/output/<id>/; nunca cria, altera nem apaga arquivo.
|
|
11
|
+
// Última linha da saída (o runner lê esta linha): CAMINHO:OK <caminho>, CAMINHO:FALTA <caminho>
|
|
12
|
+
// ou CAMINHO:REPROVADO <motivo>; o caminho sai relativo à pasta do projeto, com `/`.
|
|
13
|
+
// Código de saída: 0 sempre que a linha CAMINHO: sai · 1 = erro de uso (ação ou opção faltando,
|
|
14
|
+
// pasta sem `_opencrew/`, crew inexistente ou fora do projeto); com código 1 não há linha
|
|
15
|
+
// CAMINHO: e nada é criado.
|
|
16
|
+
// Spec: fase-r3-runner-em-uso-real.md (repositório do OpenCrew).
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { MSG, dentroDoProjeto, ehPrincipal, realDentroDe } from './comum.mjs';
|
|
19
|
+
import { USO, erroDeArgumentos, lerArgs, limpar } from './caminho/argumentos.mjs';
|
|
20
|
+
import { MOTIVO, daMaisNova, motivoDeReprovacao, naExecucao, normalizar, proximaVersao } from './caminho/nucleo.mjs';
|
|
21
|
+
import { criarPasta, ehPasta, lerTexto, pastasDe, temConteudo } from './caminho/disco.mjs';
|
|
22
|
+
|
|
23
|
+
const ok = (caminho) => `CAMINHO:OK ${caminho}`;
|
|
24
|
+
const falta = (caminho) => `CAMINHO:FALTA ${caminho}`;
|
|
25
|
+
const reprovado = (motivo) => `CAMINHO:REPROVADO ${motivo}`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* O arquivo declarado cabe na execução? Dentro de `output/`, depois de ganhar o run, ele tem de
|
|
29
|
+
* continuar dentro da pasta da execução — pelo texto e pelo lugar real.
|
|
30
|
+
* @returns {string|null} o erro de uso, ou `null`
|
|
31
|
+
*/
|
|
32
|
+
function erroDoArquivo(raiz, crew, { acao, run, arquivo }) {
|
|
33
|
+
if (!dentroDoProjeto(raiz, arquivo)) return MSG.foraDoProjeto(limpar(arquivo));
|
|
34
|
+
const local = acao === 'conferir' ? null : naExecucao(arquivo, crew, run);
|
|
35
|
+
if (!local) return null;
|
|
36
|
+
if (!local.nome) return `Falta o nome do arquivo em --arquivo: ${limpar(arquivo)}`;
|
|
37
|
+
const execucao = path.resolve(raiz, 'crews', crew, 'output', run);
|
|
38
|
+
const partes = normalizar(arquivo).split('/');
|
|
39
|
+
const sai = partes.includes('..') || partes.includes('.') || !realDentroDe(execucao, path.resolve(raiz, local.grupo));
|
|
40
|
+
return sai ? `Caminho fora da pasta da execução: ${limpar(arquivo)}` : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Onde fica a crew, ou o erro de uso. A crew é uma pasta direta de `crews/` (`crews/<nome>`
|
|
45
|
+
* também vale).
|
|
46
|
+
* @returns {{ erro: string } | { crew: string }}
|
|
47
|
+
*/
|
|
48
|
+
function localizar(raiz, args) {
|
|
49
|
+
const erro = erroDeArgumentos(args);
|
|
50
|
+
if (erro) return { erro };
|
|
51
|
+
if (!ehPasta(path.join(raiz, '_opencrew'))) return { erro: MSG.semRaiz };
|
|
52
|
+
const base = path.resolve(raiz, 'crews');
|
|
53
|
+
const nome = args.crew.replace(/^crews[\\/]+/, '');
|
|
54
|
+
if (!dentroDoProjeto(base, nome)) return { erro: MSG.foraDoProjeto(limpar(args.crew)) };
|
|
55
|
+
const pasta = path.resolve(base, nome);
|
|
56
|
+
if (path.dirname(pasta) !== base || !ehPasta(pasta)) return { erro: MSG.crewNaoEncontrada(limpar(args.crew)) };
|
|
57
|
+
const crew = path.basename(pasta);
|
|
58
|
+
const doArquivo = args.arquivo ? erroDoArquivo(raiz, crew, args) : null;
|
|
59
|
+
return doArquivo ? { erro: doArquivo } : { crew };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Regras 2 e 3: o caminho em que o passo grava; a pasta dele é criada. */
|
|
63
|
+
function saida(raiz, crew, { run, arquivo }) {
|
|
64
|
+
const local = naExecucao(arquivo, crew, run);
|
|
65
|
+
if (!local) return ok(normalizar(arquivo));
|
|
66
|
+
const pasta = `${local.grupo}/${proximaVersao(pastasDe(path.resolve(raiz, local.grupo)))}`;
|
|
67
|
+
criarPasta(path.resolve(raiz, pasta));
|
|
68
|
+
return ok(`${pasta}/${local.nome}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Regra 4: a versão mais nova que tem o arquivo; depois, o próprio grupo, sem pasta de versão. */
|
|
72
|
+
function entrada(raiz, crew, { run, arquivo }) {
|
|
73
|
+
const local = naExecucao(arquivo, crew, run);
|
|
74
|
+
const { grupo, nome } = local ?? { grupo: path.posix.dirname(normalizar(arquivo)), nome: path.posix.basename(normalizar(arquivo)) };
|
|
75
|
+
const versoes = local ? daMaisNova(pastasDe(path.resolve(raiz, grupo))) : [];
|
|
76
|
+
const candidatos = [...versoes.map((versao) => `${grupo}/${versao}/${nome}`), local ? `${grupo}/${nome}` : normalizar(arquivo)];
|
|
77
|
+
const achado = candidatos.find((caminho) => temConteudo(path.resolve(raiz, caminho)));
|
|
78
|
+
return achado ? ok(achado) : falta(candidatos.at(-1));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Regra 5: o arquivo gravado existe, não está vazio e tem o que o contrato do passo pede. */
|
|
82
|
+
function conferir(raiz, { arquivo, secoes, tldr }) {
|
|
83
|
+
const caminho = normalizar(arquivo);
|
|
84
|
+
const alvo = path.resolve(raiz, caminho);
|
|
85
|
+
if (!temConteudo(alvo)) return reprovado(MOTIVO.ausente);
|
|
86
|
+
const pedido = { secoes: secoes === undefined ? null : Number(secoes), tldr };
|
|
87
|
+
const motivo = pedido.secoes !== null || tldr ? motivoDeReprovacao(lerTexto(alvo), pedido) : null;
|
|
88
|
+
return motivo ? reprovado(motivo) : ok(caminho);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function responder(raiz, crew, args) {
|
|
92
|
+
if (args.acao === 'saida') return saida(raiz, crew, args);
|
|
93
|
+
if (args.acao === 'entrada') return entrada(raiz, crew, args);
|
|
94
|
+
if (args.acao === 'conferir') return conferir(raiz, args);
|
|
95
|
+
const pasta = `crews/${crew}/output/${args.run}`;
|
|
96
|
+
criarPasta(path.resolve(raiz, pasta));
|
|
97
|
+
return ok(pasta);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string[]} argv
|
|
102
|
+
* @param {object} [deps] `cwd` (a pasta do projeto) e `escrever`
|
|
103
|
+
* @returns {number} 0 = a linha `CAMINHO:` saiu · 1 = erro de uso, ou falha ao ler ou criar pasta
|
|
104
|
+
* (a linha de uso e o motivo, ou só o erro; sem linha `CAMINHO:`)
|
|
105
|
+
*/
|
|
106
|
+
export function main(argv, deps = {}) {
|
|
107
|
+
const { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = deps;
|
|
108
|
+
const args = lerArgs(argv);
|
|
109
|
+
const local = localizar(cwd, args);
|
|
110
|
+
if (local.erro) {
|
|
111
|
+
escrever(USO);
|
|
112
|
+
escrever(local.erro);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
escrever(responder(cwd, local.crew, args));
|
|
117
|
+
return 0;
|
|
118
|
+
} catch (erro) {
|
|
119
|
+
escrever(`Não consegui resolver o caminho: ${limpar(erro?.message ?? erro)}`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (ehPrincipal(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|