@aksp/opencrew 1.5.0 → 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 +29 -0
- package/README.md +18 -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/prompts/build.prompt.md +12 -0
- package/templates/_opencrew/core/prompts/discovery.prompt.md +15 -2
- package/templates/_opencrew/core/runner.pipeline.md +44 -6
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +189 -0
- package/templates/_opencrew/core/scripts/verificar/leitura.mjs +9 -4
- package/templates/skills/opencrew-best-practice-creator/SKILL.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
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.6.0] — 2026-10-02
|
|
7
|
+
|
|
8
|
+
Trilha U2 "Crew que conhece o projeto" + U6 "Convivência" (`specs/fase-u2-crew-que-conhece-o-projeto.md`).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`fontes:` no `crew.yaml`** — arquivos/pastas do projeto (caminho relativo) que a crew lê em todo
|
|
12
|
+
run e trata como verdade; o discovery pergunta quais são.
|
|
13
|
+
- **Conferência de fontes** (`_opencrew/core/scripts/conferir-fontes.mjs`) no início de cada run:
|
|
14
|
+
arquivo movido → acha o novo lugar e oferece corrigir (`--corrigir`, com `.bak`); nome diferente →
|
|
15
|
+
lista a pasta; caminho absoluto → alerta "não é portátil". No uso real (Projeto B) achou os 5
|
|
16
|
+
caminhos quebrados pela reorganização, cada um com o lugar exato.
|
|
17
|
+
- **Correção gravada na hora** — o que o usuário corrige num checkpoint vai para a memória antes do
|
|
18
|
+
próximo passo; termo removido vira proibição entre aspas (trava do verificador); conflito com o
|
|
19
|
+
`company.md` gera a pergunta "Atualizo o perfil da empresa?".
|
|
20
|
+
- **`_opencrew/best-practices.local/`** — best-practices do usuário (aprendidas/criadas), lidas antes
|
|
21
|
+
das do core e nunca tocadas pelo `update`; o verificador também lê os limites dali primeiro.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **`update` completo e seguro**: entrega pastas novas do framework (agentes-base, config, templates
|
|
25
|
+
de crew) sem sobrescrever; guarda em `.opencrew-backup/<data>/` o que o usuário editou antes de
|
|
26
|
+
substituir (manifesto `_opencrew/manifest.json`); recusa voltar para versão mais antiga; atualiza
|
|
27
|
+
as pontes **só das IDEs instaladas**; faz merge do Playwright no `.mcp.json` (saída em
|
|
28
|
+
`_opencrew/logs/playwright/`, outros servidores intactos); avisa sobre pontes antigas (`opensquad`).
|
|
29
|
+
- **Convivência**: pontes e bloco do `AGENTS.md` só ativam o OpenCrew com `/opencrew` (ou pedido
|
|
30
|
+
sobre crews) e apontam direto para `_opencrew/core/system.md`; outras instruções do projeto têm
|
|
31
|
+
prioridade no resto.
|
|
32
|
+
- Migração do formato de memória faz `memories.md.bak` e avisa (fim do reset silencioso); regra única
|
|
33
|
+
sobre o que vai para a memória (só feedback explícito).
|
|
34
|
+
- `_build/discovery.yaml` agora em `crews/{code}/_build/`; build grava caminhos relativos à raiz.
|
|
6
35
|
## [1.5.0] — 2026-10-02
|
|
7
36
|
|
|
8
37
|
Trilha U1 "Revisor com dentes" — primeira melhoria vinda do uso real
|
package/README.md
CHANGED
|
@@ -40,6 +40,10 @@ dentro da sua IDE.**
|
|
|
40
40
|
termos que você proibiu e `[PREENCHER]` pendentes, e aponta afirmações a confirmar.
|
|
41
41
|
Bloqueio não passa, seja qual for a nota do revisor. A crew não inventa casos nem números:
|
|
42
42
|
quando falta um dado real, ela pergunta na aprovação final.
|
|
43
|
+
- 📂 **Crew que conhece o projeto** — liste em `fontes:` os arquivos e pastas do seu projeto
|
|
44
|
+
(decisões, calendário, manual de marca) e a crew os lê em todo run, tratando-os como verdade.
|
|
45
|
+
Reorganizou as pastas? No início do run ela confere os caminhos, acha para onde o arquivo foi
|
|
46
|
+
e oferece corrigir. Correções que você faz num checkpoint ficam gravadas na hora.
|
|
43
47
|
|
|
44
48
|
---
|
|
45
49
|
|
|
@@ -201,28 +205,31 @@ meu-projeto/
|
|
|
201
205
|
## Mantendo o OpenCrew atualizado
|
|
202
206
|
|
|
203
207
|
```bash
|
|
204
|
-
npx @aksp/opencrew update
|
|
208
|
+
npx @aksp/opencrew@latest update
|
|
205
209
|
```
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
Um único comando traz **todas** as melhorias para quem já usa uma versão antiga — sem perder
|
|
212
|
+
o que você fez:
|
|
208
213
|
|
|
209
214
|
| O que é atualizado | O que NUNCA é tocado |
|
|
210
215
|
|---|---|
|
|
211
|
-
| `_opencrew/core/` (framework
|
|
212
|
-
|
|
|
213
|
-
| `_opencrew/
|
|
214
|
-
| Bloco `<!-- opencrew -->`
|
|
216
|
+
| `_opencrew/core/` (framework) e skills do catálogo | `crews/` (suas crews) |
|
|
217
|
+
| Pastas novas do framework (agentes-base, config) — só o que falta | `_opencrew/_memory/` (perfil, preferências) |
|
|
218
|
+
| Pontes das IDEs **que você já tem instaladas** (nunca cria de IDE nova) | `_opencrew/best-practices.local/` (suas best-practices) |
|
|
219
|
+
| Bloco `<!-- opencrew -->` do `AGENTS.md`/`CLAUDE.md` (o resto do arquivo fica intacto) | `.env` (suas chaves) |
|
|
220
|
+
| Servidor Playwright no `.mcp.json` (outros servidores intactos) | |
|
|
215
221
|
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
- **Editou um arquivo do framework ou um skill do catálogo?** Antes de substituir, o `update`
|
|
223
|
+
guarda a sua versão em `.opencrew-backup/<data>/` e lista o que copiou.
|
|
224
|
+
- **Versão mais nova instalada?** O `update` não volta para uma versão mais antiga (cache do
|
|
225
|
+
`npx`): ele para e pede `npx @aksp/opencrew@latest update`.
|
|
226
|
+
|
|
227
|
+
Para regravar as pontes de IDEs específicas (ou adicionar uma IDE nova):
|
|
218
228
|
|
|
219
229
|
```bash
|
|
220
230
|
npx @aksp/opencrew init --repair-bridges --ide=claude-code
|
|
221
231
|
```
|
|
222
232
|
|
|
223
|
-
(troque `claude-code` pelas IDEs que você usa, separadas por vírgula; sem `--ide`, o
|
|
224
|
-
comando grava as pontes de **todas** as IDEs suportadas).
|
|
225
|
-
|
|
226
233
|
Se você está migrando de uma versão anterior a v1.3, o `update` detecta
|
|
227
234
|
AGENTS.md legados (sistema completo de 150 linhas) e os substitui pela ponte
|
|
228
235
|
fina. Desde a v1.4.2 o arquivo original é copiado antes para `AGENTS.md.bak` (até a v1.4.1,
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { templatesDir, packageJsonPath } from '../lib/paths.js';
|
|
4
|
-
import {
|
|
4
|
+
import { exists, writeFileSafe, readJson, writeBridgeFile } from '../lib/fsx.js';
|
|
5
|
+
import { newDelivery, deliverTree, deliverFile, writeManifest, readManifest } from '../lib/manifest.js';
|
|
5
6
|
import { ideById, allIdeIds, AGENTS_BRIDGE } from '../lib/ides.js';
|
|
6
7
|
import { pickIdes as promptIdes } from '../lib/prompts.js';
|
|
7
8
|
import { UsageError } from '../lib/errors.js';
|
|
@@ -26,7 +27,10 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
|
|
|
26
27
|
const ids = await resolveIdes(opts, async () => allIdeIds());
|
|
27
28
|
log(`\n${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — repairing IDE bridges`);
|
|
28
29
|
log(c.dim(`Target: ${target}\n`));
|
|
29
|
-
await
|
|
30
|
+
const previous = await readManifest(target);
|
|
31
|
+
const repairCtx = newDelivery(target, previous);
|
|
32
|
+
await writeBridges(target, ids, { overwrite: true, ctx: repairCtx });
|
|
33
|
+
await writeManifest(target, version, { ...(previous?.files ?? {}), ...repairCtx.files });
|
|
30
34
|
|
|
31
35
|
log(`\n${c.green(c.bold('Done!'))} IDE bridges regenerated.\n`);
|
|
32
36
|
log(`${c.bold('Next step:')} Restart your IDE, then type ${c.cyan('/opencrew')} to verify.\n`);
|
|
@@ -51,7 +55,8 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
|
|
|
51
55
|
|
|
52
56
|
// 1. Copy the framework payload (never clobber user work).
|
|
53
57
|
step('Installing framework files');
|
|
54
|
-
const
|
|
58
|
+
const ctx = newDelivery(target, null);
|
|
59
|
+
const copied = await installPayload(target, ctx);
|
|
55
60
|
ok(`Framework files ready (${copied} written, existing files preserved)`);
|
|
56
61
|
|
|
57
62
|
// 2. System doc + root configs.
|
|
@@ -59,7 +64,7 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
|
|
|
59
64
|
|
|
60
65
|
// Full system definition lives in _opencrew/core/ — never at project root.
|
|
61
66
|
// The root AGENTS.md is just a thin bridge (like CLAUDE.md, GEMINI.md, etc.).
|
|
62
|
-
await
|
|
67
|
+
await deliverFile(ctx, path.join(target, '_opencrew', 'core', 'system.md'), await tpl('AGENTS.md'), { overwrite: true });
|
|
63
68
|
ok('_opencrew/core/system.md (full system definition)');
|
|
64
69
|
|
|
65
70
|
const agentsResult = await writeBridgeFile(path.join(target, 'AGENTS.md'), AGENTS_BRIDGE);
|
|
@@ -78,14 +83,16 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
|
|
|
78
83
|
|
|
79
84
|
// 3. IDE bridge files.
|
|
80
85
|
step('Configuring AI IDEs');
|
|
81
|
-
await writeBridges(target, ids, { overwrite: false });
|
|
86
|
+
await writeBridges(target, ids, { overwrite: false, ctx });
|
|
82
87
|
|
|
83
88
|
if (ids.includes('claude-code')) {
|
|
84
89
|
warn(`opencrew ships its own Playwright MCP server (.mcp.json) — disable Claude Code's`);
|
|
85
90
|
warn(`native Playwright plugin/extension to avoid the two conflicting.`);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
// 4.
|
|
93
|
+
// 4. Manifest (what OpenCrew delivered — lets `update` spot the user's edits), then the
|
|
94
|
+
// version stamp LAST: it is what marks the install as complete.
|
|
95
|
+
await writeManifest(target, version, ctx.files);
|
|
89
96
|
await fs.writeFile(path.join(target, STAMP), version + '\n');
|
|
90
97
|
|
|
91
98
|
// 5. Done.
|
|
@@ -101,21 +108,18 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
|
|
|
101
108
|
* Never copies the version stamp: only a finished init writes it.
|
|
102
109
|
* @returns {Promise<number>} files written
|
|
103
110
|
*/
|
|
104
|
-
export async function installPayload(target) {
|
|
105
|
-
|
|
106
|
-
const onCopy = () => (count += 1);
|
|
107
|
-
await copyDir(path.join(templatesDir, '_opencrew'), path.join(target, '_opencrew'), {
|
|
111
|
+
export async function installPayload(target, ctx = newDelivery(target, null)) {
|
|
112
|
+
await deliverTree(ctx, path.join(templatesDir, '_opencrew'), path.join(target, '_opencrew'), {
|
|
108
113
|
overwrite: false,
|
|
109
114
|
// Never ship stray logs, browser sessions or the template's own stamp.
|
|
110
115
|
skip: (rel) =>
|
|
111
116
|
(rel.startsWith('logs/') && rel !== 'logs/.gitkeep') ||
|
|
112
117
|
rel.startsWith('_browser_profile/') ||
|
|
113
118
|
rel === '.opencrew-version',
|
|
114
|
-
onCopy,
|
|
115
119
|
});
|
|
116
|
-
await
|
|
117
|
-
await
|
|
118
|
-
return
|
|
120
|
+
await deliverTree(ctx, path.join(templatesDir, 'skills'), path.join(target, 'skills'), { overwrite: false });
|
|
121
|
+
await deliverTree(ctx, path.join(templatesDir, 'crews'), path.join(target, 'crews'), { overwrite: false });
|
|
122
|
+
return ctx.written;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/** 'none' (no core) · 'partial' (core without stamp: interrupted install) · 'complete'. */
|
|
@@ -148,7 +152,7 @@ async function resolveIdes(opts, fallback) {
|
|
|
148
152
|
* @param {string[]} ids — validated IDE ids to configure
|
|
149
153
|
* @param {{ overwrite: boolean }} opts
|
|
150
154
|
*/
|
|
151
|
-
async function writeBridges(target, ids, { overwrite }) {
|
|
155
|
+
async function writeBridges(target, ids, { overwrite, ctx }) {
|
|
152
156
|
const writtenPaths = new Set();
|
|
153
157
|
|
|
154
158
|
for (const id of ids) {
|
|
@@ -162,7 +166,7 @@ async function writeBridges(target, ids, { overwrite }) {
|
|
|
162
166
|
const fp = path.join(target, f.path);
|
|
163
167
|
const hasFrontmatter = f.content.startsWith('---');
|
|
164
168
|
if (hasFrontmatter) {
|
|
165
|
-
await
|
|
169
|
+
await deliverFile(ctx, fp, f.content, { overwrite });
|
|
166
170
|
} else {
|
|
167
171
|
const result = await writeBridgeFile(fp, f.content);
|
|
168
172
|
if (result.merged) info(`${f.path} (merged — existing content preserved)`);
|
package/src/commands/update.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { templatesDir, packageJsonPath } from '../lib/paths.js';
|
|
4
|
-
import {
|
|
4
|
+
import { exists, writeFileSafe, readJson, writeBridgeFile, readFile } from '../lib/fsx.js';
|
|
5
5
|
import { AGENTS_BRIDGE, LEAKED_STATUS_SECTION, ideById } from '../lib/ides.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// (
|
|
6
|
+
import { readManifest, writeManifest, newDelivery, deliverTree, deliverFile } from '../lib/manifest.js';
|
|
7
|
+
import { compareVersions, detectInstalledIdes, refreshBridges, mergeMcp, findLegacyBridges } from '../lib/migrations.js';
|
|
8
|
+
import { c, log, info, ok, warn, err, step } from '../lib/ui.js';
|
|
9
|
+
|
|
10
|
+
// `update` brings EVERY improvement to people who already use OpenCrew (AGENTS.md rule 14),
|
|
11
|
+
// without losing what they made:
|
|
12
|
+
// - _opencrew/core and catalog skills are replaced — a file the user edited is copied to
|
|
13
|
+
// .opencrew-backup/<date>/ first (manifest of hashes; none = copy whatever differs);
|
|
14
|
+
// - new framework folders (agents, config, crew templates) arrive without overwriting;
|
|
15
|
+
// - bridges of the IDEs already installed are refreshed (never new IDEs);
|
|
16
|
+
// - crews/, _opencrew/_memory/, _opencrew/best-practices.local/ and .env are never touched.
|
|
15
17
|
export async function update(opts = {}) {
|
|
16
18
|
const target = process.cwd();
|
|
17
19
|
const pkg = await readJson(packageJsonPath);
|
|
@@ -27,14 +29,15 @@ export async function update(opts = {}) {
|
|
|
27
29
|
const current = (await exists(versionFile))
|
|
28
30
|
? (await fs.readFile(versionFile, 'utf8')).trim()
|
|
29
31
|
: 'unknown';
|
|
32
|
+
const newer = current !== 'unknown' && compareVersions(current, version) > 0;
|
|
30
33
|
|
|
31
34
|
log(`\n${c.bold(c.cyan('opencrew update'))}`);
|
|
32
35
|
log(c.dim(`Installed: ${current} → Package: ${version}\n`));
|
|
33
36
|
|
|
34
37
|
if (opts.check) {
|
|
35
|
-
if (current === version) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
if (current === version) ok(`Up to date (v${version}).`);
|
|
39
|
+
else if (newer) info(`A versão instalada (v${current}) é mais nova que este pacote (v${version}).`);
|
|
40
|
+
else {
|
|
38
41
|
info(`Update available: v${current} → v${version}.`);
|
|
39
42
|
info(`Run ${c.cyan('npx @aksp/opencrew update')} to apply.`);
|
|
40
43
|
process.exitCode = 1;
|
|
@@ -42,45 +45,58 @@ export async function update(opts = {}) {
|
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
if (
|
|
46
|
-
|
|
48
|
+
if (newer) {
|
|
49
|
+
err(`Você tem a v${current} instalada e este pacote é a v${version} (mais antigo). Nada foi alterado.`);
|
|
50
|
+
info(`Use ${c.cyan('npx @aksp/opencrew@latest update')}.`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
const manifest = await readManifest(target);
|
|
56
|
+
const ctx = newDelivery(target, manifest);
|
|
57
|
+
const tpl = (...p) => path.join(templatesDir, ...p);
|
|
58
|
+
const dest = (...p) => path.join(target, ...p);
|
|
59
|
+
|
|
50
60
|
step('Refreshing framework');
|
|
51
|
-
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Skill directories that only exist in the user's project — i.e. not part of
|
|
61
|
-
// the catalog — are never touched, since copyDir only visits paths that exist
|
|
62
|
-
// in the source (templates/skills/).
|
|
63
|
-
step('Refreshing catalog skills');
|
|
64
|
-
warn('Catalog skills are fully overwritten — your edits to any built-in skill files will be lost.');
|
|
65
|
-
let s = 0;
|
|
66
|
-
await copyDir(path.join(templatesDir, 'skills'), path.join(target, 'skills'), {
|
|
67
|
-
overwrite: true,
|
|
68
|
-
onCopy: () => (s += 1),
|
|
69
|
-
});
|
|
70
|
-
ok(`Catalog skills refreshed (${s} files)`);
|
|
71
|
-
|
|
72
|
-
// System doc — full definition in _opencrew/core/, thin bridge at root.
|
|
73
|
-
const systemContent = await fs.readFile(path.join(templatesDir, 'AGENTS.md'), 'utf8');
|
|
74
|
-
await writeFileSafe(path.join(target, '_opencrew', 'core', 'system.md'), systemContent);
|
|
75
|
-
ok('_opencrew/core/system.md refreshed');
|
|
61
|
+
await deliverTree(ctx, tpl('_opencrew', 'core'), dest('_opencrew', 'core'), { overwrite: true });
|
|
62
|
+
await deliverFile(ctx, dest('_opencrew', 'core', 'system.md'), await fs.readFile(tpl('AGENTS.md')), { overwrite: true });
|
|
63
|
+
await deliverTree(ctx, tpl('skills'), dest('skills'), { overwrite: true });
|
|
64
|
+
// New framework folders (e.g. base agents since 1.3.2): only what is missing.
|
|
65
|
+
for (const dir of ['agents', 'config', '_investigations']) {
|
|
66
|
+
await deliverTree(ctx, tpl('_opencrew', dir), dest('_opencrew', dir), { overwrite: false });
|
|
67
|
+
}
|
|
68
|
+
await deliverTree(ctx, tpl('crews'), dest('crews'), { overwrite: false });
|
|
69
|
+
ok(`Framework and catalog skills refreshed (${ctx.written} files written)`);
|
|
76
70
|
|
|
71
|
+
step('Refreshing IDE bridges');
|
|
77
72
|
await refreshAgentsBridge(target);
|
|
73
|
+
const ides = await detectInstalledIdes(target);
|
|
74
|
+
await refreshBridges(ctx, ides);
|
|
75
|
+
ok(ides.length ? `Bridges refreshed: ${ides.map((i) => i.label).join(', ')}` : 'No IDE bridges found to refresh');
|
|
78
76
|
await removeLeakedStatusSection(target);
|
|
79
77
|
|
|
78
|
+
const mcp = await mergeMcp(target, tpl('.mcp.json'));
|
|
79
|
+
if (mcp === 'updated' || mcp === 'created') ok(`.mcp.json (Playwright: ${mcp === 'created' ? 'created' : 'saída em _opencrew/logs/playwright/'})`);
|
|
80
|
+
if (mcp === 'invalid') warn('.mcp.json não é um JSON válido — não alterado. Confira o arquivo.');
|
|
81
|
+
|
|
82
|
+
for (const legacy of await findLegacyBridges(target)) {
|
|
83
|
+
warn(`Ponte antiga encontrada: ${legacy} (aponta para _opensquad/, que não existe neste projeto). Pode apagar com segurança.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ctx.copied.length) {
|
|
87
|
+
const rel = path.relative(target, ctx.backupDir).split(path.sep).join('/');
|
|
88
|
+
const what = manifest ? 'que você tinha editado' : 'diferentes do pacote novo';
|
|
89
|
+
warn(`${ctx.copied.length} arquivo(s) ${what} foram copiados para ${rel}/ antes de serem substituídos:`);
|
|
90
|
+
for (const f of ctx.copied.slice(0, 15)) log(` ${f}`);
|
|
91
|
+
if (ctx.copied.length > 15) log(` … e mais ${ctx.copied.length - 15}`);
|
|
92
|
+
if (!manifest) info('Primeira atualização com proteção: sem registro anterior, guardamos tudo o que diferia. Daqui em diante, só o que você editar.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await writeManifest(target, version, ctx.files);
|
|
80
96
|
// Stamp last: a crash above leaves the old version, so the next update retries.
|
|
81
97
|
await fs.writeFile(versionFile, version + '\n');
|
|
82
98
|
log(`\n${c.green(c.bold('Updated to v' + version))}.`);
|
|
83
|
-
log(c.dim('Your crews, memory,
|
|
99
|
+
log(c.dim('Your crews, memory, local best-practices and .env were left untouched.\n'));
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
// Root AGENTS.md: create it if missing; a legacy full-system doc (pre-v1.3) is backed up
|
package/src/lib/ides.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
// Single source of truth =
|
|
2
|
-
// Every IDE gets only a THIN bridge file that points at
|
|
1
|
+
// Single source of truth = _opencrew/core/system.md (from templates/AGENTS.md).
|
|
2
|
+
// Every IDE gets only a THIN bridge file that points at it.
|
|
3
3
|
// Adding support for a new IDE = one more entry in this list.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Coexistence (U6): opencrew only takes over when called — other agent systems in the same
|
|
6
|
+
// project keep priority for everything else.
|
|
7
|
+
const ACTIVATION = `Use opencrew ONLY when the user types \`/opencrew\` or asks to create, run or manage
|
|
8
|
+
AI agent crews. In that case, read \`_opencrew/core/system.md\` and follow its initialization,
|
|
9
|
+
command routing and workflow instructions. For anything else, the other instructions of this
|
|
10
|
+
project take precedence.`;
|
|
11
|
+
|
|
12
|
+
const BRIDGE = `${ACTIVATION}
|
|
7
13
|
|
|
8
14
|
If invoked with arguments (e.g. \`/opencrew create ...\`, \`/opencrew run ...\`),
|
|
9
|
-
route to the matching action from the Command Routing table in
|
|
15
|
+
route to the matching action from the Command Routing table in \`_opencrew/core/system.md\`.
|
|
10
16
|
If invoked without arguments, show the Main Menu.`;
|
|
11
17
|
|
|
12
18
|
// Claude Code needs one extra rule (checkpoints must use AskUserQuestion) and a
|
|
@@ -20,7 +26,7 @@ description: "opencrew — multi-agent orchestration. Use when the user types /o
|
|
|
20
26
|
|
|
21
27
|
${BRIDGE}
|
|
22
28
|
|
|
23
|
-
## Claude Code specifics (override
|
|
29
|
+
## Claude Code specifics (override system.md where they conflict)
|
|
24
30
|
|
|
25
31
|
- **Checkpoints MUST use \`AskUserQuestion\`** — never output a checkpoint question as plain text.
|
|
26
32
|
Combine multiple questions into a single call (max 4 slots, each with 2–4 options).
|
|
@@ -32,7 +38,8 @@ ${BRIDGE}
|
|
|
32
38
|
const CLAUDE_MD = `# opencrew — Project Instructions
|
|
33
39
|
|
|
34
40
|
This project uses **opencrew**, a multi-agent orchestration framework.
|
|
35
|
-
|
|
41
|
+
|
|
42
|
+
${ACTIVATION}
|
|
36
43
|
|
|
37
44
|
Type \`/opencrew\` to open the main menu.
|
|
38
45
|
|
|
@@ -44,11 +51,7 @@ Type \`/opencrew\` to open the main menu.
|
|
|
44
51
|
`;
|
|
45
52
|
|
|
46
53
|
// Root AGENTS.md: thin bridge to the full system definition (written by init and update).
|
|
47
|
-
export const AGENTS_BRIDGE =
|
|
48
|
-
+ 'The opencrew system definition lives at `_opencrew/core/system.md`.\n'
|
|
49
|
-
+ 'Read that file and adopt the opencrew system role — follow all initialization,\n'
|
|
50
|
-
+ 'command routing, and workflow instructions defined there.\n\n'
|
|
51
|
-
+ 'Type `/opencrew` to open the main menu.\n';
|
|
54
|
+
export const AGENTS_BRIDGE = `# opencrew\n\n${ACTIVATION}\n\nType \`/opencrew\` to open the main menu.\n`;
|
|
52
55
|
|
|
53
56
|
// Marker that identifies the maintainer STATUS.md section leaked into CLAUDE.md by 1.4.0/1.4.1.
|
|
54
57
|
export const LEAKED_STATUS_SECTION = '## STATUS.md (gestão de sessão)';
|
|
@@ -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,110 @@
|
|
|
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).
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { exists, writeBridgeFile } from './fsx.js';
|
|
6
|
+
import { IDES } from './ides.js';
|
|
7
|
+
import { deliverFile } from './manifest.js';
|
|
8
|
+
|
|
9
|
+
/** Semver compare (no pre-release tags): >0 if a > b, <0 if a < b, 0 if equal. */
|
|
10
|
+
export function compareVersions(a, b) {
|
|
11
|
+
const pa = String(a).split('.').map(Number);
|
|
12
|
+
const pb = String(b).split('.').map(Number);
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
15
|
+
if (d) return d;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sharedPaths = (() => {
|
|
21
|
+
const count = new Map();
|
|
22
|
+
for (const ide of IDES) for (const f of ide.files) count.set(f.path, (count.get(f.path) ?? 0) + 1);
|
|
23
|
+
return new Set([...count].filter(([, n]) => n > 1).map(([p]) => p));
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
async function hasOpencrew(file) {
|
|
27
|
+
return (await exists(file)) && /opencrew/i.test(await fs.readFile(file, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* IDEs installed in `target`, detected by their own bridge files (a path shared by several
|
|
32
|
+
* IDEs only counts for an IDE that has no file of its own — e.g. Codex).
|
|
33
|
+
*/
|
|
34
|
+
export async function detectInstalledIdes(target) {
|
|
35
|
+
const found = [];
|
|
36
|
+
for (const ide of IDES) {
|
|
37
|
+
const own = ide.files.filter((f) => !sharedPaths.has(f.path));
|
|
38
|
+
const probes = own.length ? own : ide.files;
|
|
39
|
+
for (const f of probes) {
|
|
40
|
+
if (await hasOpencrew(path.join(target, f.path))) {
|
|
41
|
+
found.push(ide);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Rewrite the bridges of the installed IDEs only (frontmatter files whole, others by block). */
|
|
50
|
+
export async function refreshBridges(ctx, ides) {
|
|
51
|
+
const done = new Set();
|
|
52
|
+
for (const ide of ides) {
|
|
53
|
+
for (const f of ide.files) {
|
|
54
|
+
if (done.has(f.path)) continue;
|
|
55
|
+
done.add(f.path);
|
|
56
|
+
const file = path.join(ctx.target, f.path);
|
|
57
|
+
if (f.content.startsWith('---')) await deliverFile(ctx, file, f.content, { overwrite: true });
|
|
58
|
+
else await writeBridgeFile(file, f.content);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const OUTPUT_DIR = ['--output-dir', '_opencrew/logs/playwright'];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Merge the Playwright server of the template into the project's .mcp.json without touching
|
|
67
|
+
* other servers. @returns 'created' | 'updated' | 'unchanged' | 'invalid'
|
|
68
|
+
*/
|
|
69
|
+
export async function mergeMcp(target, templateFile) {
|
|
70
|
+
const file = path.join(target, '.mcp.json');
|
|
71
|
+
const template = JSON.parse(await fs.readFile(templateFile, 'utf8'));
|
|
72
|
+
if (!(await exists(file))) {
|
|
73
|
+
await fs.writeFile(file, JSON.stringify(template, null, 2) + '\n');
|
|
74
|
+
return 'created';
|
|
75
|
+
}
|
|
76
|
+
let current;
|
|
77
|
+
try {
|
|
78
|
+
current = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
79
|
+
} catch {
|
|
80
|
+
return 'invalid';
|
|
81
|
+
}
|
|
82
|
+
current.mcpServers ??= {};
|
|
83
|
+
const pw = current.mcpServers.playwright;
|
|
84
|
+
if (!pw) current.mcpServers.playwright = template.mcpServers.playwright;
|
|
85
|
+
else if (Array.isArray(pw.args) && !pw.args.includes('--output-dir')) pw.args.push(...OUTPUT_DIR);
|
|
86
|
+
else return 'unchanged';
|
|
87
|
+
await fs.writeFile(file, JSON.stringify(current, null, 2) + '\n');
|
|
88
|
+
return 'updated';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const LEGACY_ROOTS = ['.gemini/skills', '.claude/skills', '.agents/skills', '.agent/workflows', '.agent/rules'];
|
|
92
|
+
|
|
93
|
+
/** Old bridges that point to a system no longer installed (e.g. `_opensquad/`). Never deleted. */
|
|
94
|
+
export async function findLegacyBridges(target) {
|
|
95
|
+
if (await exists(path.join(target, '_opensquad'))) return [];
|
|
96
|
+
const found = [];
|
|
97
|
+
async function walk(dir, depth) {
|
|
98
|
+
let entries;
|
|
99
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
100
|
+
for (const e of entries) {
|
|
101
|
+
const p = path.join(dir, e.name);
|
|
102
|
+
if (e.isDirectory() && depth < 2) await walk(p, depth + 1);
|
|
103
|
+
else if (e.isFile() && e.name.endsWith('.md') && /_opensquad\//.test(await fs.readFile(p, 'utf8'))) {
|
|
104
|
+
found.push(path.relative(target, p).split(path.sep).join('/'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const root of LEGACY_ROOTS) await walk(path.join(target, root), 0);
|
|
109
|
+
return found;
|
|
110
|
+
}
|
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.0
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -530,6 +560,14 @@ Apply this transformation consistently for every write in this step.
|
|
|
530
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."
|
|
531
561
|
- Wait for user input before proceeding
|
|
532
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.
|
|
533
571
|
- **If the step frontmatter contains `outputFile`**: after collecting the user's full response,
|
|
534
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.
|
|
535
573
|
Use this format:
|
|
@@ -766,7 +804,7 @@ This archives the run state for the `runs` command while keeping crew history av
|
|
|
766
804
|
- Run scores, review grades, output file paths, topics from past runs
|
|
767
805
|
|
|
768
806
|
**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
|
|
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)
|
|
770
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
|
|
771
809
|
|
|
772
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
|
+
}
|
|
@@ -37,11 +37,16 @@ export function semFrontmatter(texto) {
|
|
|
37
37
|
return texto.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
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
|
+
*/
|
|
41
44
|
export async function lerLimites(raiz, formatoId) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -15,7 +15,7 @@ version: "2.0.0"
|
|
|
15
15
|
|
|
16
16
|
# Best-Practice Creator — Workflow
|
|
17
17
|
|
|
18
|
-
Use this workflow when creating a new best-practice file
|
|
18
|
+
Use this workflow when creating a new best-practice file. New and customized best-practices live in `_opencrew/best-practices.local/` — the user's own library, read before `_opencrew/core/best-practices/` and never touched by `npx @aksp/opencrew update` (the core folder is replaced on every update).
|
|
19
19
|
|
|
20
20
|
## Pre-flight Checks
|
|
21
21
|
|
|
@@ -67,7 +67,7 @@ For each existing best-practice file whose scope overlaps with the new one:
|
|
|
67
67
|
|
|
68
68
|
### 2. Update `_catalog.yaml`
|
|
69
69
|
|
|
70
|
-
Add a new entry to `_opencrew/
|
|
70
|
+
Add a new entry to `_opencrew/best-practices.local/_catalog.yaml` (create it if missing) with:
|
|
71
71
|
- `id`: matching the frontmatter `id`
|
|
72
72
|
- `name`: matching the frontmatter `name`
|
|
73
73
|
- `whenToUse`: single-line summary of the scope (positive only, no "NOT for")
|
|
@@ -77,7 +77,7 @@ Place it under the appropriate section comment (Discipline or Platform best prac
|
|
|
77
77
|
|
|
78
78
|
### 3. File placement
|
|
79
79
|
|
|
80
|
-
Save to `_opencrew/
|
|
80
|
+
Save to `_opencrew/best-practices.local/{id}.md`.
|
|
81
81
|
|
|
82
82
|
### 4. Validation
|
|
83
83
|
|
|
@@ -93,7 +93,7 @@ Re-read the created file and verify:
|
|
|
93
93
|
|
|
94
94
|
# Best-Practice Updater — Workflow
|
|
95
95
|
|
|
96
|
-
Use this workflow when updating best-practice files
|
|
96
|
+
Use this workflow when updating best-practice files. To change a core best-practice, first copy it from `_opencrew/core/best-practices/` to `_opencrew/best-practices.local/` and edit the copy there (the local copy overrides the core one and survives updates).
|
|
97
97
|
|
|
98
98
|
## Versioning Rules (Semver)
|
|
99
99
|
|