@aksp/opencrew 1.5.0 → 1.6.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 +90 -0
- package/README.md +35 -14
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/init.js +42 -30
- 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 +147 -0
- package/templates/.mcp.json +1 -1
- package/templates/AGENTS.md +2 -1
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/prompts/build.prompt.md +14 -0
- package/templates/_opencrew/core/prompts/discovery.prompt.md +15 -2
- package/templates/_opencrew/core/runner.pipeline.md +86 -20
- package/templates/_opencrew/core/scripts/comum.mjs +48 -0
- package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +82 -0
- package/templates/_opencrew/core/scripts/conferir-fontes/coleta.mjs +104 -0
- package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +52 -0
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +135 -0
- package/templates/_opencrew/core/scripts/verificar/argumentos.mjs +33 -0
- package/templates/_opencrew/core/scripts/verificar/arquivos.mjs +50 -0
- package/templates/_opencrew/core/scripts/verificar/html.mjs +52 -0
- package/templates/_opencrew/core/scripts/verificar/leitura.mjs +138 -66
- package/templates/_opencrew/core/scripts/verificar/medicao.mjs +142 -0
- package/templates/_opencrew/core/scripts/verificar/pecas.mjs +190 -0
- package/templates/_opencrew/core/scripts/verificar/proibicoes.mjs +100 -0
- package/templates/_opencrew/core/scripts/verificar/regras.mjs +110 -91
- package/templates/_opencrew/core/scripts/verificar/relatorio.mjs +43 -0
- package/templates/_opencrew/core/scripts/verificar/secoes.mjs +145 -0
- package/templates/_opencrew/core/scripts/verificar.mjs +162 -88
- package/templates/skills/opencrew-best-practice-creator/SKILL.md +4 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Manifest of the files OpenCrew delivered (path → sha256). It lets `update` tell a file the
|
|
2
|
+
// user edited (copy it to .opencrew-backup/ before replacing) from one that is just older.
|
|
3
|
+
// Workspaces without a manifest (≤ 1.5.0): any file that differs from the new package is copied.
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { exists } from './fsx.js';
|
|
8
|
+
|
|
9
|
+
export const MANIFEST = path.join('_opencrew', 'manifest.json');
|
|
10
|
+
|
|
11
|
+
// Text files are hashed with LF line endings: an editor, git or a sync tool switching CRLF/LF
|
|
12
|
+
// must not make a file look "edited by the user". Binary files (with NUL bytes) are hashed as is.
|
|
13
|
+
const normalize = (buf) => {
|
|
14
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
15
|
+
return b.includes(0) ? b : Buffer.from(b.toString('utf8').replace(/\r\n/g, '\n'));
|
|
16
|
+
};
|
|
17
|
+
const sha = (buf) => createHash('sha256').update(normalize(buf)).digest('hex');
|
|
18
|
+
const rel = (target, abs) => path.relative(target, abs).split(path.sep).join('/');
|
|
19
|
+
|
|
20
|
+
/** @returns {Promise<{files: Record<string,string>} | null>} null = no (or unreadable) manifest */
|
|
21
|
+
export async function readManifest(target) {
|
|
22
|
+
try {
|
|
23
|
+
const data = JSON.parse(await fs.readFile(path.join(target, MANIFEST), 'utf8'));
|
|
24
|
+
return data && typeof data.files === 'object' ? data : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeManifest(target, version, files) {
|
|
31
|
+
const sorted = Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)));
|
|
32
|
+
await fs.writeFile(path.join(target, MANIFEST), JSON.stringify({ version, files: sorted }, null, 2) + '\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Delivery context shared by one init/update run.
|
|
37
|
+
* @param {string} target project root
|
|
38
|
+
* @param {{files:Record<string,string>}|null} manifest previous manifest (null = none)
|
|
39
|
+
*/
|
|
40
|
+
export function newDelivery(target, manifest) {
|
|
41
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
42
|
+
return { target, manifest, backupDir: path.join(target, '.opencrew-backup', stamp), files: {}, copied: [], written: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Put `content` at `dest`. Missing → write. Equal → nothing. Different → only if `overwrite`,
|
|
47
|
+
* copying the current file to the backup dir first when the user edited it.
|
|
48
|
+
*/
|
|
49
|
+
export async function deliverFile(ctx, dest, content, { overwrite }) {
|
|
50
|
+
const key = rel(ctx.target, dest);
|
|
51
|
+
const fresh = sha(content);
|
|
52
|
+
if (!(await exists(dest))) {
|
|
53
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
54
|
+
await fs.writeFile(dest, content);
|
|
55
|
+
ctx.files[key] = fresh;
|
|
56
|
+
ctx.written += 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const current = sha(await fs.readFile(dest));
|
|
60
|
+
if (current === fresh) {
|
|
61
|
+
ctx.files[key] = fresh;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!overwrite) return; // the user's version stays (and is not tracked)
|
|
65
|
+
const edited = ctx.manifest ? ctx.manifest.files[key] !== current : true;
|
|
66
|
+
if (edited) {
|
|
67
|
+
const copy = path.join(ctx.backupDir, key);
|
|
68
|
+
await fs.mkdir(path.dirname(copy), { recursive: true });
|
|
69
|
+
await fs.copyFile(dest, copy);
|
|
70
|
+
ctx.copied.push(key);
|
|
71
|
+
}
|
|
72
|
+
await fs.writeFile(dest, content);
|
|
73
|
+
ctx.files[key] = fresh;
|
|
74
|
+
ctx.written += 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** deliverFile for every file under `src`, mirrored into `dest`. */
|
|
78
|
+
export async function deliverTree(ctx, src, dest, { overwrite, skip = () => false }) {
|
|
79
|
+
async function walk(dir) {
|
|
80
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
81
|
+
const from = path.join(dir, entry.name);
|
|
82
|
+
const relPath = path.relative(src, from).split(path.sep).join('/');
|
|
83
|
+
if (skip(relPath)) continue;
|
|
84
|
+
if (entry.isDirectory()) await walk(from);
|
|
85
|
+
else await deliverFile(ctx, path.join(dest, relPath), await fs.readFile(from), { overwrite });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await walk(src);
|
|
89
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// What `update` does beyond refreshing _opencrew/core and the catalog skills, so that every
|
|
2
|
+
// improvement reaches people who already use OpenCrew (AGENTS.md rule 14). The IDE detection
|
|
3
|
+
// is shared with `init --repair-bridges`, whose helpers live here too.
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { exists, writeBridgeFile } from './fsx.js';
|
|
7
|
+
import { IDES, allIdeIds } from './ides.js';
|
|
8
|
+
import { deliverFile, writeManifest } from './manifest.js';
|
|
9
|
+
|
|
10
|
+
/** Semver compare (no pre-release tags): >0 if a > b, <0 if a < b, 0 if equal. */
|
|
11
|
+
export function compareVersions(a, b) {
|
|
12
|
+
const pa = String(a).split('.').map(Number);
|
|
13
|
+
const pb = String(b).split('.').map(Number);
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
16
|
+
if (d) return d;
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sharedPaths = (() => {
|
|
22
|
+
const count = new Map();
|
|
23
|
+
for (const ide of IDES) for (const f of ide.files) count.set(f.path, (count.get(f.path) ?? 0) + 1);
|
|
24
|
+
return new Set([...count].filter(([, n]) => n > 1).map(([p]) => p));
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
async function hasOpencrew(file) {
|
|
28
|
+
return (await exists(file)) && /opencrew/i.test(await fs.readFile(file, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* IDEs installed in `target`, detected by their own bridge files (a path shared by several
|
|
33
|
+
* IDEs only counts for an IDE that has no file of its own — e.g. Codex).
|
|
34
|
+
*/
|
|
35
|
+
export async function detectInstalledIdes(target) {
|
|
36
|
+
const found = [];
|
|
37
|
+
for (const ide of IDES) {
|
|
38
|
+
const own = ide.files.filter((f) => !sharedPaths.has(f.path));
|
|
39
|
+
const probes = own.length ? own : ide.files;
|
|
40
|
+
for (const f of probes) {
|
|
41
|
+
if (await hasOpencrew(path.join(target, f.path))) {
|
|
42
|
+
found.push(ide);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* IDE ids `init --repair-bridges` rewrites when --ide is not given: every IDE with --all,
|
|
52
|
+
* otherwise the ones `update` would detect. --yes chooses nothing here.
|
|
53
|
+
*/
|
|
54
|
+
export async function repairIdeIds(target, { all } = {}) {
|
|
55
|
+
if (all) return allIdeIds();
|
|
56
|
+
return (await detectInstalledIdes(target)).map((ide) => ide.id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `init --repair-bridges` found no bridge and got neither --ide nor --all. */
|
|
60
|
+
export const NO_BRIDGES_FOUND =
|
|
61
|
+
`Não encontrei pontes de IDE aqui. Use \`--ide=<id>\` para escolher. Ids válidos: ${allIdeIds().join(', ')}.`;
|
|
62
|
+
|
|
63
|
+
/** `init --repair-bridges` in a folder that is not a workspace: the repair never installs. */
|
|
64
|
+
export const NO_WORKSPACE =
|
|
65
|
+
'Não encontrei um workspace do OpenCrew nesta pasta. O reparo não instala: para instalar, rode `npx @aksp/opencrew@latest init`.';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Add the bridges a repair rewrote to the manifest — only when the workspace has one. Without
|
|
69
|
+
* it (installed up to 1.5.0) none is created: a bridges-only manifest would make the next
|
|
70
|
+
* `update` call every older file "edited by you" and hide its first-protected-update notice.
|
|
71
|
+
*/
|
|
72
|
+
export async function recordRepair(ctx, version) {
|
|
73
|
+
if (ctx.manifest) await writeManifest(ctx.target, version, { ...ctx.manifest.files, ...ctx.files });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Summary lines of a delivery's backup copies, each with its path in .opencrew-backup/<date>/. */
|
|
77
|
+
export function backupSummary(ctx) {
|
|
78
|
+
if (!ctx.copied.length) return [];
|
|
79
|
+
const dir = path.relative(ctx.target, ctx.backupDir).split(path.sep).join('/');
|
|
80
|
+
return [
|
|
81
|
+
`${ctx.copied.length} cópia(s) de segurança feita(s) antes de regravar:`,
|
|
82
|
+
...ctx.copied.map((file) => ` ${dir}/${file}`),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Rewrite the bridges of the installed IDEs only (frontmatter files whole, others by block). */
|
|
87
|
+
export async function refreshBridges(ctx, ides) {
|
|
88
|
+
const done = new Set();
|
|
89
|
+
for (const ide of ides) {
|
|
90
|
+
for (const f of ide.files) {
|
|
91
|
+
if (done.has(f.path)) continue;
|
|
92
|
+
done.add(f.path);
|
|
93
|
+
const file = path.join(ctx.target, f.path);
|
|
94
|
+
if (f.content.startsWith('---')) await deliverFile(ctx, file, f.content, { overwrite: true });
|
|
95
|
+
else await writeBridgeFile(file, f.content);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const OUTPUT_DIR = ['--output-dir', '_opencrew/logs/playwright'];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merge the Playwright server of the template into the project's .mcp.json without touching
|
|
104
|
+
* other servers. @returns 'created' | 'updated' | 'unchanged' | 'invalid'
|
|
105
|
+
*/
|
|
106
|
+
export async function mergeMcp(target, templateFile) {
|
|
107
|
+
const file = path.join(target, '.mcp.json');
|
|
108
|
+
const template = JSON.parse(await fs.readFile(templateFile, 'utf8'));
|
|
109
|
+
if (!(await exists(file))) {
|
|
110
|
+
await fs.writeFile(file, JSON.stringify(template, null, 2) + '\n');
|
|
111
|
+
return 'created';
|
|
112
|
+
}
|
|
113
|
+
let current;
|
|
114
|
+
try {
|
|
115
|
+
current = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
return 'invalid';
|
|
118
|
+
}
|
|
119
|
+
current.mcpServers ??= {};
|
|
120
|
+
const pw = current.mcpServers.playwright;
|
|
121
|
+
if (!pw) current.mcpServers.playwright = template.mcpServers.playwright;
|
|
122
|
+
else if (Array.isArray(pw.args) && !pw.args.includes('--output-dir')) pw.args.push(...OUTPUT_DIR);
|
|
123
|
+
else return 'unchanged';
|
|
124
|
+
await fs.writeFile(file, JSON.stringify(current, null, 2) + '\n');
|
|
125
|
+
return 'updated';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const LEGACY_ROOTS = ['.gemini/skills', '.claude/skills', '.agents/skills', '.agent/workflows', '.agent/rules'];
|
|
129
|
+
|
|
130
|
+
/** Old bridges that point to a system no longer installed (e.g. `_opensquad/`). Never deleted. */
|
|
131
|
+
export async function findLegacyBridges(target) {
|
|
132
|
+
if (await exists(path.join(target, '_opensquad'))) return [];
|
|
133
|
+
const found = [];
|
|
134
|
+
async function walk(dir, depth) {
|
|
135
|
+
let entries;
|
|
136
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
137
|
+
for (const e of entries) {
|
|
138
|
+
const p = path.join(dir, e.name);
|
|
139
|
+
if (e.isDirectory() && depth < 2) await walk(p, depth + 1);
|
|
140
|
+
else if (e.isFile() && e.name.endsWith('.md') && /_opensquad\//.test(await fs.readFile(p, 'utf8'))) {
|
|
141
|
+
found.push(path.relative(target, p).split(path.sep).join('/'));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const root of LEGACY_ROOTS) await walk(path.join(target, root), 0);
|
|
146
|
+
return found;
|
|
147
|
+
}
|
package/templates/.mcp.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"mcpServers": {
|
|
4
4
|
"playwright": {
|
|
5
5
|
"command": "npx",
|
|
6
|
-
"args": ["@playwright/mcp@0.0.78", "--config", "_opencrew/config/playwright.config.json"]
|
|
6
|
+
"args": ["@playwright/mcp@0.0.78", "--config", "_opencrew/config/playwright.config.json", "--output-dir", "_opencrew/logs/playwright"]
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
}
|
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.1
|
|
@@ -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
|
|
@@ -398,6 +410,8 @@ side_effects: irreversible # REQUIRED for any step that publishes, posts, sends
|
|
|
398
410
|
# distributes outside the project (it cannot be undone). The Pipeline
|
|
399
411
|
# Runner never retries these automatically, and Gate 2c places them last.
|
|
400
412
|
# Omit for every other step.
|
|
413
|
+
max_review_cycles: {N} # ONLY for the review step: write it next to its `on_reject`.
|
|
414
|
+
# By crew tier (`crew.tier` in design.yaml): Express 1, Standard 2, Full 3.
|
|
401
415
|
---
|
|
402
416
|
```
|
|
403
417
|
|
|
@@ -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,35 @@ 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. **Source check** — before loading the project sources (1d), run:
|
|
89
|
+
```bash
|
|
90
|
+
node _opencrew/core/scripts/conferir-fontes.mjs --crew "crews/{name}"
|
|
91
|
+
```
|
|
92
|
+
If the last line is `FONTES:PENDENTE` (a cited file was moved, renamed or deleted), show the
|
|
93
|
+
report and ask — never continue silently with a missing source:
|
|
94
|
+
```
|
|
95
|
+
Alguns arquivos que a crew usa não estão mais onde ela espera:
|
|
96
|
+
{resumo do relatório}
|
|
97
|
+
|
|
98
|
+
1. Corrigir os caminhos sugeridos (troco nos arquivos da crew e guardo .bak)
|
|
99
|
+
2. Seguir assim mesmo
|
|
100
|
+
3. Parar
|
|
101
|
+
```
|
|
102
|
+
On 1, run the same command with `--corrigir`, show the new result and re-read `crew.yaml` and
|
|
103
|
+
any agent file already loaded (it may have changed them); 1d then loads the sources from the
|
|
104
|
+
corrected paths. If the new result still ends in `FONTES:PENDENTE`, ask again with options 2 and
|
|
105
|
+
3 only. Not-portable alerts (absolute paths) are mentioned once, without stopping. If the script
|
|
106
|
+
did not run (no Node, an error, or no `FONTES:` status line), tell the user "⚠️ A conferência de
|
|
107
|
+
fontes não rodou: {motivo}" and continue; the final approval repeats the warning.
|
|
108
|
+
|
|
109
|
+
1d. **Project sources (`fontes:`)** — if `crew.yaml` has a `fontes:` list (files or folders of
|
|
110
|
+
the user's project, paths relative to the project root), read them now: a file in full up to
|
|
111
|
+
~300 lines, otherwise its headings plus the passages relevant to this run's task; a folder as
|
|
112
|
+
its file list. Treat them as the **truth of the project**: when they disagree with the
|
|
113
|
+
briefing, the research or your own assumptions, the sources take precedence over them
|
|
114
|
+
(as fontes valem sobre o briefing e a pesquisa) — and say so when it matters.
|
|
83
115
|
|
|
84
116
|
2. Read `crews/{name}/pipeline/pipeline.yaml` for the pipeline definition
|
|
85
117
|
3. **Resolve skills**: Read `crew.yaml` → `skills` section. For each non-native skill (anything other than web_search, web_fetch):
|
|
@@ -261,8 +293,10 @@ Before executing any step that references an agent:
|
|
|
261
293
|
- The agent must follow the export process for the specified format — read the input file,
|
|
262
294
|
transform the content, and write the output file in the target format.
|
|
263
295
|
- Skip the best-practices lookup below for export formats.
|
|
264
|
-
b. **Content formats** — otherwise, read `_opencrew/
|
|
265
|
-
|
|
296
|
+
b. **Content formats** — otherwise, read `_opencrew/best-practices.local/{format}.md` (the user's
|
|
297
|
+
own version, never touched by `update`) if it exists, else `_opencrew/core/best-practices/{format}.md`
|
|
298
|
+
(e.g., `_opencrew/core/best-practices/instagram-feed.md`)
|
|
299
|
+
- 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
300
|
c. Parse the YAML frontmatter to extract the `name` field
|
|
267
301
|
d. Extract the Markdown body (everything after the YAML frontmatter closing `---`)
|
|
268
302
|
e. Append to the agent's context, before skill instructions:
|
|
@@ -319,6 +353,16 @@ Before executing any step that references an agent:
|
|
|
319
353
|
- Faltou um dado real? Escreva [PREENCHER: o que falta] no lugar — o usuário completa
|
|
320
354
|
na aprovação final. Um [PREENCHER] honesto vale mais que um exemplo inventado.
|
|
321
355
|
```
|
|
356
|
+
g. **Reviewer rules (always)** — for every step with `on_reject:`, inject at the same point:
|
|
357
|
+
```
|
|
358
|
+
--- REGRAS DO REVISOR ---
|
|
359
|
+
- Copie os valores medidos do relatório; nunca estime contagens.
|
|
360
|
+
- Bloqueio no relatório é REJECT, seja qual for a nota — menos [PREENCHER], que o usuário
|
|
361
|
+
resolve na aprovação final.
|
|
362
|
+
- Alerta não resolvido nem justificado limita a nota a 7/10.
|
|
363
|
+
- O checklist só marca o que o relatório confirma; item "não medido" ou "não verificado" é
|
|
364
|
+
dito assim, nunca como aprovado.
|
|
365
|
+
```
|
|
322
366
|
|
|
323
367
|
### Context Compression (Summary-Based Handoff)
|
|
324
368
|
|
|
@@ -530,6 +574,15 @@ Apply this transformation consistently for every write in this step.
|
|
|
530
574
|
- **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."
|
|
531
575
|
- Wait for user input before proceeding
|
|
532
576
|
- Save the user's choice/response for the next step
|
|
577
|
+
- **Correction → memory, right away**: if the answer corrects something (tone, audience, a term,
|
|
578
|
+
a fact, a format), write it to `crews/{name}/_memory/memories.md` in the matching section
|
|
579
|
+
**before the next step** (antes do próximo passo) — not only at the end of the run, which may
|
|
580
|
+
never come. A term the user asked to remove goes to `## Proibições Explícitas` **between
|
|
581
|
+
quotes** (entre aspas), in the canonical form — `- Nunca usar "termo"` or, with a replacement,
|
|
582
|
+
`- Nunca usar "termo" → usar "outro"` — so the automatic checker blocks it next time.
|
|
583
|
+
- **Correction vs. company profile**: if the correction contradicts `_opencrew/_memory/company.md`
|
|
584
|
+
(e.g. the organization's name, the main audience), ask: "Isso vale para todas as crews?
|
|
585
|
+
Atualizo o perfil da empresa?" — change `company.md` only after a yes.
|
|
533
586
|
- **If the step frontmatter contains `outputFile`**: after collecting the user's full response,
|
|
534
587
|
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.
|
|
535
588
|
Use this format:
|
|
@@ -640,36 +693,48 @@ catching obvious issues early and reducing review cycle waste.
|
|
|
640
693
|
When a step has `on_reject: {step-id}` (a review step):
|
|
641
694
|
|
|
642
695
|
1. **Automatic check BEFORE the reviewer runs** — run the checker on **all outputs** (todas as
|
|
643
|
-
saídas) of every non-checkpoint step from the `on_reject` step up to the step right before
|
|
644
|
-
|
|
696
|
+
saídas) of every non-checkpoint step from the `on_reject` step up to the step right before the
|
|
697
|
+
review, using the transformed paths of this run (run_id/vN). Each item is `caminho=formato`, with
|
|
698
|
+
the `format:` of the step that generated that file; a step with no `format:`, with an export
|
|
699
|
+
format (`pdf`, `csv`, `formatted-post`) or with one outside `[a-z0-9-]+` goes without `=formato`:
|
|
645
700
|
```bash
|
|
646
|
-
node _opencrew/core/scripts/verificar.mjs --crew crews/{name} --arquivo "{path1},{path2},…"
|
|
701
|
+
node _opencrew/core/scripts/verificar.mjs --crew "crews/{name}" --arquivo "{path1}={format1},{path2},…"
|
|
647
702
|
```
|
|
648
703
|
Save the full output to `crews/{name}/output/{run_id}/verificacao-ciclo-{N}.md` and inject it
|
|
649
704
|
into the reviewer's context as `--- VERIFICAÇÃO AUTOMÁTICA ---`. The reviewer must copy the
|
|
650
|
-
measured values from it (see best-practices `review.md`). If the
|
|
651
|
-
|
|
652
|
-
|
|
705
|
+
measured values from it (see best-practices `review.md`). If the checker did not run (no Node,
|
|
706
|
+
an error, or no `VERIFICACAO:` status line), tell the user, continue with the normal review and
|
|
707
|
+
repeat it at the final approval: "⚠️ A verificação automática não rodou: {motivo}".
|
|
653
708
|
2. **A block cannot be approved** — if the last line of the checker output is
|
|
654
709
|
`VERIFICACAO:BLOQUEADA`, the verdict is **REJECT** regardless of the score (qualquer que seja a
|
|
655
710
|
nota). Send the report (blocks first) to the writer together with the reviewer's feedback.
|
|
656
711
|
If the last line is `VERIFICACAO:AGUARDANDO_USUARIO`, the only blocks are `[PREENCHER: …]`
|
|
657
712
|
(real data only the user has): do NOT reject for them — the reviewer judges the rest, and the
|
|
658
713
|
final approval below collects the missing data from the user.
|
|
659
|
-
3. Track the review cycle count
|
|
660
|
-
|
|
714
|
+
3. Track the review cycle count: a **cycle** is one pass of the reviewer. The maximum is
|
|
715
|
+
`max_review_cycles`, an integer from 1 declared where the step declares `on_reject` (the step
|
|
716
|
+
frontmatter or its `pipeline.yaml` entry); absent or invalid: 3. On every rejection, with or
|
|
717
|
+
without a block, send the reviewer's feedback to the writer and go back to the referenced step.
|
|
718
|
+
4. If the last allowed pass also rejects, stop; the status of the last report picks the message, as
|
|
719
|
+
in item 2 — `VERIFICACAO:BLOQUEADA`: the blocks; any other status: the reviewer's feedback, also
|
|
720
|
+
with `VERIFICACAO:AGUARDANDO_USUARIO` (its only blocks are `[PREENCHER: …]`). Same three options:
|
|
661
721
|
```
|
|
662
|
-
⚠️ A revisão ainda encontra bloqueios depois de {N} ciclos:
|
|
722
|
+
{if VERIFICACAO:BLOQUEADA} ⚠️ A revisão ainda encontra bloqueios depois de {N} ciclos:
|
|
663
723
|
{lista de bloqueios do relatório}
|
|
724
|
+
{any other status} A revisão não aprovou o texto depois de {N} ciclos. Motivo: {parecer resumido}
|
|
664
725
|
|
|
665
726
|
1. Corrigir eu mesmo (eu edito o texto e você verifica de novo)
|
|
666
|
-
2. Aceitar assim mesmo
|
|
727
|
+
2. Aceitar assim mesmo
|
|
667
728
|
3. Abortar
|
|
668
729
|
```
|
|
669
730
|
5. **Final approval checkpoint** (the checkpoint after the review): show the summary of the last
|
|
670
|
-
report — `Verificação automática: {N} bloqueios, {M} alertas` — plus the list
|
|
671
|
-
|
|
672
|
-
|
|
731
|
+
report — `Verificação automática: {N} bloqueios, {M} alertas, {Z} não medidos` — plus the list
|
|
732
|
+
of alerts and the {Z} items not measured or not verified (the `Não medido` and `Não verificado`
|
|
733
|
+
lines under each file, not the "não é texto" line of **Notas**), one per line as
|
|
734
|
+
`{arquivo} — {motivo}`, then the lines under `**Notas:**` in that report, as they are written,
|
|
735
|
+
and repeat every "não rodou" warning of this run (checker and source check). If the approved
|
|
736
|
+
text still contains `[PREENCHER: …]`, ask the user for each missing piece of real information
|
|
737
|
+
and write it into the text before approving.
|
|
673
738
|
|
|
674
739
|
### Dashboard Handoff (between steps)
|
|
675
740
|
|
|
@@ -758,7 +823,8 @@ This archives the run state for the `runs` command while keeping crew history av
|
|
|
758
823
|
- Writing style choices → `## Estilo de Escrita`
|
|
759
824
|
- Visual/design preferences → `## Design Visual`
|
|
760
825
|
- Content structure choices → `## Estrutura de Conteúdo`
|
|
761
|
-
- Explicit rejections or prohibitions → `## Proibições Explícitas
|
|
826
|
+
- Explicit rejections or prohibitions → `## Proibições Explícitas`, in the canonical form
|
|
827
|
+
(`- Nunca usar "termo"` or `- Nunca usar "termo" → usar "outro"`)
|
|
762
828
|
- Crew-specific technical patterns → `## Técnico (específico do crew)`
|
|
763
829
|
|
|
764
830
|
**Never write to `memories.md`:**
|
|
@@ -766,7 +832,7 @@ This archives the run state for the `runs` command while keeping crew history av
|
|
|
766
832
|
- Run scores, review grades, output file paths, topics from past runs
|
|
767
833
|
|
|
768
834
|
**Technical routing:** For any technical learning (bugs, workarounds, API behavior):
|
|
769
|
-
- If it affects any crew (Playwright bugs, OS rendering quirks, API limits) → write to
|
|
835
|
+
- 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)
|
|
770
836
|
- 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
|
|
771
837
|
|
|
772
838
|
After applying all candidates, write the updated `memories.md`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Validações e mensagens de erro de uso comuns aos scripts do runtime (verificar,
|
|
2
|
+
// conferir-fontes…). Node puro, sem dependências.
|
|
3
|
+
// Spec: specs/fase-r1-reparos-1-6-1.md, regra 13 (repositório do OpenCrew).
|
|
4
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
export const MSG = {
|
|
9
|
+
faltaOpcao: (opcao) => `Falta a opção obrigatória ${opcao}.`,
|
|
10
|
+
semRaiz: 'Não encontrei `_opencrew/` nesta pasta. Rode o comando a partir da pasta do projeto.',
|
|
11
|
+
foraDoProjeto: (caminho) => `Caminho fora do projeto: ${caminho}`,
|
|
12
|
+
crewNaoEncontrada: (crew) => `Crew não encontrada: ${crew}`,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** O caminho (relativo à raiz ou absoluto) fica dentro da raiz do projeto? */
|
|
16
|
+
export function dentroDoProjeto(raiz, caminho) {
|
|
17
|
+
const rel = path.relative(raiz, path.resolve(raiz, caminho));
|
|
18
|
+
return !(rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* O script foi chamado direto (`node …/script.mjs`)? Compara os caminhos reais: com o projeto
|
|
23
|
+
* aberto por uma junção ou um link de pasta, o Node resolve o link em `import.meta.url` e não em
|
|
24
|
+
* `process.argv[1]`, e a comparação dos textos daria falso — o script sairia sem imprimir nada.
|
|
25
|
+
*/
|
|
26
|
+
export function ehPrincipal(metaUrl) {
|
|
27
|
+
try {
|
|
28
|
+
return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(metaUrl));
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ehPasta = (p) => existsSync(p) && statSync(p).isDirectory();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Erro de uso, na ordem da regra 13: opção obrigatória faltando → pasta atual sem `_opencrew/`
|
|
38
|
+
* → crew ou caminho fora do projeto (antes de testar se existe) → crew inexistente.
|
|
39
|
+
* @returns {string|null} a mensagem em PT-BR, ou null quando está tudo certo
|
|
40
|
+
*/
|
|
41
|
+
export function erroDeUso({ raiz, faltando = [], crew, caminhos = [] }) {
|
|
42
|
+
if (faltando.length) return MSG.faltaOpcao(faltando[0]);
|
|
43
|
+
if (!ehPasta(path.join(raiz, '_opencrew'))) return MSG.semRaiz;
|
|
44
|
+
const fora = [crew, ...caminhos].find((c) => !dentroDoProjeto(raiz, c));
|
|
45
|
+
if (fora) return MSG.foraDoProjeto(fora);
|
|
46
|
+
if (!ehPasta(path.resolve(raiz, crew))) return MSG.crewNaoEncontrada(crew);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Busca da conferência de fontes: onde está cada caminho citado (crew → raiz do projeto →
|
|
2
|
+
// absoluto → ao lado do agente ou da task que cita) e, quando ele sumiu, o que existe no projeto
|
|
3
|
+
// com o mesmo nome. Só testa existência e lista nomes; nunca lê conteúdo.
|
|
4
|
+
// Spec: specs/fase-r1-reparos-1-6-1.md, regra 17 (repositório do OpenCrew).
|
|
5
|
+
import { readdir } from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
export const LIMITE_DA_BUSCA = 20000;
|
|
10
|
+
const IGNORAR = new Set(['node_modules', 'output', '_opencrew', '_build']);
|
|
11
|
+
|
|
12
|
+
export const barra = (p) => p.split(path.sep).join('/');
|
|
13
|
+
export const ehAbsoluto = (p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('/');
|
|
14
|
+
export const temBarraFinal = (ref) => /[\\/]$/.test(ref);
|
|
15
|
+
const semBarraFinal = (ref) => ref.replace(/[\\/]+$/, '');
|
|
16
|
+
|
|
17
|
+
const SUFIXO_DE_AGENTE = '.agent.md';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pastas ao lado de quem cita, só para arquivo de agente ou de task (dentro de `agents/`): a do
|
|
21
|
+
* próprio arquivo e, para `agents/X.agent.md`, também `agents/X/` — é lá que fica a task que o
|
|
22
|
+
* frontmatter do agente escreve como `tasks/x.md`.
|
|
23
|
+
*/
|
|
24
|
+
function pastasDeQuemCita(raiz, crew, citadoEm) {
|
|
25
|
+
const agentes = path.resolve(raiz, crew, 'agents') + path.sep;
|
|
26
|
+
return citadoEm.filter((arquivo) => arquivo.startsWith(agentes)).flatMap((arquivo) => {
|
|
27
|
+
const pasta = path.dirname(arquivo);
|
|
28
|
+
const nome = path.basename(arquivo);
|
|
29
|
+
return nome.endsWith(SUFIXO_DE_AGENTE) ? [pasta, path.join(pasta, nome.slice(0, -SUFIXO_DE_AGENTE.length))] : [pasta];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Caminho real do que foi citado, ou null quando não existe. Ordem: pasta da crew → raiz do
|
|
35
|
+
* projeto → absoluto → pastas ao lado dos arquivos de agente ou de task que citam (`citadoEm`).
|
|
36
|
+
*/
|
|
37
|
+
export function resolver(raiz, crew, ref, citadoEm = []) {
|
|
38
|
+
if (ehAbsoluto(ref)) return existsSync(ref) ? path.resolve(ref) : null;
|
|
39
|
+
for (const base of [path.resolve(raiz, crew), raiz, ...pastasDeQuemCita(raiz, crew, citadoEm)]) {
|
|
40
|
+
const p = path.resolve(base, ref);
|
|
41
|
+
if (existsSync(p)) return p;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Índice nome → caminhos relativos à raiz (pasta termina em `/`), sem saídas, dependências e
|
|
48
|
+
* pastas ocultas. Para ao passar de `limite` itens: aí `parcial` é true.
|
|
49
|
+
*/
|
|
50
|
+
export async function indexar(raiz, limite) {
|
|
51
|
+
const porNome = new Map();
|
|
52
|
+
let vistos = 0;
|
|
53
|
+
let parcial = false;
|
|
54
|
+
async function percorrer(dir) {
|
|
55
|
+
let entradas;
|
|
56
|
+
try { entradas = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
57
|
+
for (const e of entradas) {
|
|
58
|
+
if (++vistos > limite) { parcial = true; return; }
|
|
59
|
+
if (e.name.startsWith('.') || (e.isDirectory() && IGNORAR.has(e.name))) continue;
|
|
60
|
+
const abs = path.join(dir, e.name);
|
|
61
|
+
const chave = e.name.toLowerCase();
|
|
62
|
+
if (!porNome.has(chave)) porNome.set(chave, []);
|
|
63
|
+
porNome.get(chave).push(barra(path.relative(raiz, abs)) + (e.isDirectory() ? '/' : ''));
|
|
64
|
+
if (e.isDirectory()) await percorrer(abs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await percorrer(raiz);
|
|
68
|
+
return { porNome, parcial };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Candidatos com o mesmo nome. Citado com barra final só casa com pasta; sem ela, com os dois. */
|
|
72
|
+
export function candidatosPorNome(indice, ref) {
|
|
73
|
+
const todos = indice.porNome.get(path.basename(semBarraFinal(ref)).toLowerCase()) ?? [];
|
|
74
|
+
return temBarraFinal(ref) ? todos.filter((c) => c.endsWith('/')) : todos;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Nomes do que existe na pasta em que o caminho deveria estar. */
|
|
78
|
+
export async function nomesDaPastaEsperada(raiz, crew, ref) {
|
|
79
|
+
const pai = resolver(raiz, crew, path.dirname(semBarraFinal(ref)));
|
|
80
|
+
if (!pai) return [];
|
|
81
|
+
try { return (await readdir(pai)).filter((f) => !f.startsWith('.')); } catch { return []; }
|
|
82
|
+
}
|