@aksp/opencrew 1.6.3 → 1.7.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 +51 -0
- package/README.md +50 -5
- package/package.json +1 -1
- package/templates/AGENTS.md +20 -6
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/escritorio/animacao.js +64 -0
- package/templates/_opencrew/core/escritorio/app.js +137 -0
- package/templates/_opencrew/core/escritorio/cena.js +132 -0
- package/templates/_opencrew/core/escritorio/demo.js +79 -0
- package/templates/_opencrew/core/escritorio/escala.js +27 -0
- package/templates/_opencrew/core/escritorio/index.html +166 -0
- package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
- package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
- package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
- package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
- package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
- package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
- package/templates/_opencrew/core/escritorio/modelo.js +29 -0
- package/templates/_opencrew/core/escritorio/painel.js +120 -0
- package/templates/_opencrew/core/escritorio/quadro.js +106 -0
- package/templates/_opencrew/core/escritorio/rota.js +62 -0
- package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
- package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
- package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
- package/templates/_opencrew/core/escritorio/sprites.js +187 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
- package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
- package/templates/_opencrew/core/runner.pipeline.md +45 -128
- package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
- package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
- package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
- package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
- package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
- package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
- package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
- package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
- package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
- package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
- package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
- package/templates/_opencrew/core/scripts/estado.mjs +96 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Os desenhos do escritório são matrizes de pixels escritas como texto: uma string por linha, uma
|
|
2
|
+
// letra por pixel e uma paleta (letra → cor). Nenhuma imagem entra no pacote.
|
|
3
|
+
// Este módulo traz as cores fixas de toda a cena, o molde do boneco de 16×16 com as trocas de
|
|
4
|
+
// linha de cada pose, e a conta que transforma uma matriz nos retângulos que a cena preenche.
|
|
5
|
+
// Os irmãos `sprites-sala.js` e `sprites-mesa.js` trazem a sala e os móveis.
|
|
6
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regra 16 (repositório do OpenCrew).
|
|
7
|
+
|
|
8
|
+
/** As cores fixas da cena. A camisa, o cabelo e a pele de cada boneco vêm do modelo. */
|
|
9
|
+
export const CORES = Object.freeze({
|
|
10
|
+
tinta: '#1a1c2c',
|
|
11
|
+
branco: '#f4f4ee',
|
|
12
|
+
cinzaClaro: '#c3c8d4',
|
|
13
|
+
cinzaEscuro: '#4a5266',
|
|
14
|
+
pisoClaro: '#9aa5b8',
|
|
15
|
+
pisoEscuro: '#8d98ad',
|
|
16
|
+
junta: '#808ba1',
|
|
17
|
+
parede: '#ead9b5',
|
|
18
|
+
paredeSombra: '#d6c196',
|
|
19
|
+
madeiraLuz: '#dba76a',
|
|
20
|
+
madeira: '#c58a52',
|
|
21
|
+
madeiraMedia: '#a0683f',
|
|
22
|
+
madeiraEscura: '#6f4630',
|
|
23
|
+
ceu: '#7ec4e0',
|
|
24
|
+
ceuClaro: '#bfe6f0',
|
|
25
|
+
folha: '#4c9a52',
|
|
26
|
+
folhaEscura: '#2f6b40',
|
|
27
|
+
telaApagada: '#232838',
|
|
28
|
+
telaAcesa: '#123f4a',
|
|
29
|
+
verde: '#5fd47a',
|
|
30
|
+
ambar: '#f4a73b',
|
|
31
|
+
vermelho: '#e0564c',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Uma matriz de pixels: `linhas` do mesmo tamanho e a `paleta` com a cor de cada letra. O ponto
|
|
36
|
+
* é sempre o pixel vazio.
|
|
37
|
+
*/
|
|
38
|
+
export function matriz(paleta, linhas) {
|
|
39
|
+
return Object.freeze({ paleta: Object.freeze({ '.': null, ...paleta }), linhas: Object.freeze(linhas) });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* O molde do boneco: 16×16, de frente, parado. Na paleta, `cabelo`, `pele`, `camisa` e as duas
|
|
44
|
+
* sombras são fendas que `coresDoBoneco` troca pelas cores de cada agente.
|
|
45
|
+
*/
|
|
46
|
+
export const MOLDE = matriz({
|
|
47
|
+
o: CORES.tinta, h: 'cabelo', s: 'pele', S: 'pele-sombra', c: 'camisa', C: 'camisa-sombra',
|
|
48
|
+
p: CORES.cinzaEscuro, P: CORES.telaApagada, b: CORES.madeiraEscura, w: CORES.branco, g: CORES.cinzaClaro,
|
|
49
|
+
}, [
|
|
50
|
+
'....oooooooo....',
|
|
51
|
+
'...ohhhhhhhho...',
|
|
52
|
+
'..ohhhhhhhhhho..',
|
|
53
|
+
'..ohhhhhhhhhho..',
|
|
54
|
+
'..ohhsssssshho..',
|
|
55
|
+
'..ohsssssssSho..',
|
|
56
|
+
'..ossossssosSo..',
|
|
57
|
+
'..osssssssssSo..',
|
|
58
|
+
'...oSSSSSSSSo...',
|
|
59
|
+
'..oCccccccccCo..',
|
|
60
|
+
'..oCccccccccCo..',
|
|
61
|
+
'..osccccccccso..',
|
|
62
|
+
'...oCCCCCCCCo...',
|
|
63
|
+
'....oppppppo....',
|
|
64
|
+
'....oppoopPo....',
|
|
65
|
+
'...obbboobbbo...',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* As poses são o molde com linhas trocadas: cada troca diz o número da linha e o texto novo.
|
|
70
|
+
* `digitar` e `andar` têm dois quadros; `mao` é o braço levantado de quem espera; `festa`, os
|
|
71
|
+
* dois braços para cima de quem comemora; `papel` é o braço com o papel, que se soma às pernas
|
|
72
|
+
* de quem anda.
|
|
73
|
+
*/
|
|
74
|
+
export const TROCAS = Object.freeze({
|
|
75
|
+
'digitar-a': { 11: '..oCccccccccso..', 12: '...ossCCCCCCo...' },
|
|
76
|
+
'digitar-b': { 11: '..osccccccccCo..', 12: '...oCCCCCCsso...' },
|
|
77
|
+
'andar-a': { 14: '....oppoobbo....', 15: '...obbbo.oo.....' },
|
|
78
|
+
'andar-b': { 14: '....obboopPo....', 15: '.....oo.obbbo...' },
|
|
79
|
+
mao: {
|
|
80
|
+
0: '....oooooooo.oo.',
|
|
81
|
+
1: '...ohhhhhhhhosso',
|
|
82
|
+
2: '..ohhhhhhhhhhoso',
|
|
83
|
+
3: '..ohhhhhhhhhhoso',
|
|
84
|
+
4: '..ohhsssssshhoCo',
|
|
85
|
+
5: '..ohsssssssShoCo',
|
|
86
|
+
6: '..ossossssosSoCo',
|
|
87
|
+
7: '..osssssssssSoCo',
|
|
88
|
+
8: '...oSSSSSSSSoCCo',
|
|
89
|
+
9: '..oCcccccccccCo.',
|
|
90
|
+
11: '..osccccccccCo..',
|
|
91
|
+
},
|
|
92
|
+
festa: {
|
|
93
|
+
0: '.oo.oooooooo.oo.',
|
|
94
|
+
1: 'ossohhhhhhhhosso',
|
|
95
|
+
2: 'osohhhhhhhhhhoso',
|
|
96
|
+
3: 'osohhhhhhhhhhoso',
|
|
97
|
+
4: 'oCohhsssssshhoCo',
|
|
98
|
+
5: 'oCohsssssssShoCo',
|
|
99
|
+
6: 'oCossossssosSoCo',
|
|
100
|
+
7: 'oCosssssssssSoCo',
|
|
101
|
+
8: 'oCCoSSSSSSSSoCCo',
|
|
102
|
+
9: '.oCccccccccccCo.',
|
|
103
|
+
11: '..oCccccccccCo..',
|
|
104
|
+
},
|
|
105
|
+
papel: {
|
|
106
|
+
8: '...oSSSSSSSSooo.',
|
|
107
|
+
9: '..oCccccccccCwwo',
|
|
108
|
+
10: '..oCccccccccCwgo',
|
|
109
|
+
11: '..osccccccccswwo',
|
|
110
|
+
12: '...oCCCCCCCCoooo',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const virar = (linha) => [...linha].reverse().join('');
|
|
115
|
+
const POSTAS = new Map();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* As 16 linhas do boneco numa pose. A mesma pose devolve sempre a mesma lista.
|
|
119
|
+
* @param {string} pose uma das poses de `quadro` (`parado` é o molde)
|
|
120
|
+
* @param {{ papel?: boolean, festa?: boolean, espelhado?: boolean }} [o] `papel`: leva o papel ·
|
|
121
|
+
* `festa`: os dois braços para cima, no lugar da pose · `espelhado`: virado para a esquerda
|
|
122
|
+
*/
|
|
123
|
+
export function linhasDoBoneco(pose, { papel = false, festa = false, espelhado = false } = {}) {
|
|
124
|
+
const chave = `${pose}|${papel}|${festa}|${espelhado}`;
|
|
125
|
+
if (!POSTAS.has(chave)) {
|
|
126
|
+
const trocas = { ...(festa ? TROCAS.festa : TROCAS[pose]), ...(papel ? TROCAS.papel : null) };
|
|
127
|
+
const linhas = MOLDE.linhas.map((linha, i) => trocas[i] ?? linha);
|
|
128
|
+
POSTAS.set(chave, espelhado ? linhas.map(virar) : linhas);
|
|
129
|
+
}
|
|
130
|
+
return POSTAS.get(chave);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A cor um quarto mais escura: a sombra que dá volume à pele e à camisa. */
|
|
134
|
+
export function sombra(cor) {
|
|
135
|
+
const canal = (inicio) => Math.round(parseInt(cor.slice(inicio, inicio + 2), 16) * 0.75).toString(16).padStart(2, '0');
|
|
136
|
+
return `#${canal(1)}${canal(3)}${canal(5)}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const PALETAS = new Map();
|
|
140
|
+
|
|
141
|
+
/** A paleta do molde com as cores de um agente (`{ camisa, cabelo, pele }`, em `#rrggbb`). */
|
|
142
|
+
export function coresDoBoneco({ camisa, cabelo, pele }) {
|
|
143
|
+
const chave = `${camisa}|${cabelo}|${pele}`;
|
|
144
|
+
if (!PALETAS.has(chave)) {
|
|
145
|
+
const fendas = { cabelo, pele, 'pele-sombra': sombra(pele), camisa, 'camisa-sombra': sombra(camisa) };
|
|
146
|
+
PALETAS.set(chave, Object.fromEntries(Object.entries(MOLDE.paleta).map(([letra, cor]) => [letra, fendas[cor] ?? cor])));
|
|
147
|
+
}
|
|
148
|
+
return PALETAS.get(chave);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Os trechos de uma linha: `[x, largura, letra]` de cada sequência da mesma letra, sem os vazios. */
|
|
152
|
+
function trechos(linha) {
|
|
153
|
+
return [...linha.matchAll(/([^.])\1*/g)].map((achado) => [achado.index, achado[0].length, achado[1]]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const guardar = (lista, item) => {
|
|
157
|
+
lista.push(item);
|
|
158
|
+
return item;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Trechos iguais em linhas seguidas viram um retângulo só: a cena preenche menos vezes. */
|
|
162
|
+
function juntar(linhas) {
|
|
163
|
+
const todos = [];
|
|
164
|
+
let acima = new Map();
|
|
165
|
+
linhas.forEach((linha, y) => {
|
|
166
|
+
const nesta = new Map();
|
|
167
|
+
for (const [x, largura, letra] of trechos(linha)) {
|
|
168
|
+
const chave = `${x}:${largura}:${letra}`;
|
|
169
|
+
const retangulo = acima.get(chave) ?? guardar(todos, [x, y, largura, 0, letra]);
|
|
170
|
+
retangulo[3] += 1;
|
|
171
|
+
nesta.set(chave, retangulo);
|
|
172
|
+
}
|
|
173
|
+
acima = nesta;
|
|
174
|
+
});
|
|
175
|
+
return todos;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const RETANGULOS = new WeakMap();
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Os retângulos que desenham uma matriz: `[x, y, largura, altura, letra]`, a partir do canto
|
|
182
|
+
* superior esquerdo dela. A conta é feita uma vez por lista de linhas.
|
|
183
|
+
*/
|
|
184
|
+
export function retangulos(linhas) {
|
|
185
|
+
if (!RETANGULOS.has(linhas)) RETANGULOS.set(linhas, juntar(linhas));
|
|
186
|
+
return RETANGULOS.get(linhas);
|
|
187
|
+
}
|
|
@@ -131,9 +131,9 @@ Generate these files. Use the Write tool for all file creation — never use Bas
|
|
|
131
131
|
- **`displayName` is REQUIRED and MUST be byte-for-byte identical to the agent's
|
|
132
132
|
`name:` frontmatter field in its `.agent.md`** (the mandatory two-word "FirstName
|
|
133
133
|
LastName" persona name). The Pipeline Runner reads `displayName` — NOT `title` — to
|
|
134
|
-
render the agent's name in
|
|
135
|
-
|
|
136
|
-
of the persona name, the crew renders with functions but no names.
|
|
134
|
+
render the agent's name in "🤖 {name} is working…" announcements, and the Escritório
|
|
135
|
+
(the optional live view) shows the same column. If `displayName` is missing, empty, or set
|
|
136
|
+
to the role/title instead of the persona name, the crew renders with functions but no names.
|
|
137
137
|
- `id` = the `path` basename with `./agents/` and `.agent.md` stripped
|
|
138
138
|
(e.g. `./agents/researcher.agent.md` → `researcher`).
|
|
139
139
|
- `title` = the agent's `title:` frontmatter (the role/function label). This is a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Repair — Fix Crew Agent Names / Manifest
|
|
2
2
|
|
|
3
3
|
You are the opencrew Repair agent. Your job is to fix an **already-created** crew whose
|
|
4
|
-
agents show their function/role but not their persona names (e.g. the
|
|
5
|
-
Pipeline Runner
|
|
4
|
+
agents show their function/role but not their persona names (e.g. the Escritório and the
|
|
5
|
+
Pipeline Runner show "Pesquisador" instead of "Pedro Pesquisa").
|
|
6
6
|
|
|
7
7
|
This is a known defect in crews built by older versions: the `crew-party.csv` manifest was
|
|
8
8
|
generated without a `displayName` column (or with the role/title in it instead of the
|
|
@@ -13,10 +13,9 @@ re-run the Build phase.
|
|
|
13
13
|
|
|
14
14
|
## Scope
|
|
15
15
|
|
|
16
|
-
You may ONLY touch files under `crews/{code}/`:
|
|
16
|
+
You may ONLY touch these files under `crews/{code}/`:
|
|
17
17
|
- `crews/{code}/crew-party.csv`
|
|
18
18
|
- `crews/{code}/agents/*.agent.md` (only in the fallback case — see Step 4)
|
|
19
|
-
- `crews/{code}/state.json` (only if it exists)
|
|
20
19
|
|
|
21
20
|
Never modify `_opencrew/`, `templates/`, or any other crew. Use the Write tool for all file
|
|
22
21
|
writes (never Bash `mkdir`).
|
|
@@ -77,13 +76,7 @@ existed and must be generated now, following the **Agent Naming Convention** fro
|
|
|
77
76
|
Only do this for agents that are actually broken. Agents that already have a valid two-word
|
|
78
77
|
`name:` are left untouched (only the CSV is rewritten to carry it).
|
|
79
78
|
|
|
80
|
-
## Step 5:
|
|
81
|
-
|
|
82
|
-
If `crews/{code}/state.json` exists, update each agent entry's `name` field to the repaired
|
|
83
|
-
`displayName`. Do not change any other field. If the file does not exist, skip — the
|
|
84
|
-
Pipeline Runner recreates it from the CSV on the next run.
|
|
85
|
-
|
|
86
|
-
## Step 6: Report
|
|
79
|
+
## Step 5: Report
|
|
87
80
|
|
|
88
81
|
Present a summary table of what changed:
|
|
89
82
|
|
|
@@ -96,11 +89,13 @@ Crew "{name}" repaired.
|
|
|
96
89
|
| copywriter | Guilherme | ✍️ Guilherme Gancho | generated |
|
|
97
90
|
|
|
98
91
|
crew-party.csv: rewritten with displayName column
|
|
99
|
-
state.json: {updated | not present}
|
|
100
92
|
|
|
101
93
|
Run it: /opencrew run {code}
|
|
102
94
|
```
|
|
103
95
|
|
|
96
|
+
The Escritório (the optional live view) takes the names from the CSV when the next run starts:
|
|
97
|
+
there is nothing else to refresh.
|
|
98
|
+
|
|
104
99
|
If nothing was broken (CSV already had a valid `displayName` for every agent), say so
|
|
105
100
|
plainly instead of inventing changes: "This crew's manifest is already correct — no repair
|
|
106
101
|
needed."
|
|
@@ -33,18 +33,7 @@ Before starting execution:
|
|
|
33
33
|
- Crew memory from `crews/{name}/_memory/memories.md`
|
|
34
34
|
- User preferences from `_opencrew/_memory/preferences.md`
|
|
35
35
|
|
|
36
|
-
1a. **
|
|
37
|
-
optional, opt-in feature that most installs never use (it requires running the
|
|
38
|
-
separate dashboard app from source — see README). Scan the already-loaded
|
|
39
|
-
`preferences.md` for a `Dashboard:` field:
|
|
40
|
-
- If its value is `enabled` (as written by onboarding: `- **Dashboard:** enabled`, or the
|
|
41
|
-
plain form `Dashboard: enabled`) → set `dashboard_enabled = true` for this run.
|
|
42
|
-
- Otherwise (`disabled`, missing, or preferences.md not configured yet) →
|
|
43
|
-
set `dashboard_enabled = false`. This is the default.
|
|
44
|
-
Store `dashboard_enabled` in working memory for the rest of this run. Every
|
|
45
|
-
`state.json` read/write instruction in this document is conditional on it —
|
|
46
|
-
when `false`, skip ALL of them; never create, update, or delete
|
|
47
|
-
`crews/{name}/state.json`.
|
|
36
|
+
1a. **Escritório toggle** — the optional live view is off unless `preferences.md` turns it on (see "Escritório" below).
|
|
48
37
|
|
|
49
38
|
> **Note on language**: The structural labels listed below are **fixed PT-BR** and must
|
|
50
39
|
> never be translated — opencrew's primary supported audience is PT-BR (see AGENTS.md →
|
|
@@ -244,41 +233,43 @@ Before starting execution:
|
|
|
244
233
|
- If it does (sub-second collision), append `-2`, `-3`, etc. until the folder does not exist
|
|
245
234
|
- Create the folder using Bash: `mkdir -p "crews/{name}/output/{run_id}"`
|
|
246
235
|
- Store `run_id` in working memory for this run — it will be used for ALL output paths
|
|
247
|
-
6. **
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
236
|
+
6. **Escritório** — if it is on, run `iniciar`, then one `pular` per deselected agent, one after the other (see "Escritório" below).
|
|
237
|
+
|
|
238
|
+
## Escritório (optional live view)
|
|
239
|
+
|
|
240
|
+
A local page that shows the crew at work, off by default. Follow this section only when the
|
|
241
|
+
already-loaded `preferences.md` has `Dashboard: enabled` (written `- **Dashboard:** enabled` or
|
|
242
|
+
plain `Dashboard: enabled`, any letter case); otherwise run none of these commands. When it is on,
|
|
243
|
+
run via Bash, from the project root, the one-line command of each moment:
|
|
244
|
+
|
|
245
|
+
| Moment | Command |
|
|
246
|
+
|---|---|
|
|
247
|
+
| Start of the run (Initialization, step 6) | `node _opencrew/core/scripts/estado.mjs "{name}" iniciar --passos {N}` |
|
|
248
|
+
| Right after `iniciar`, once per deselected agent | `node _opencrew/core/scripts/estado.mjs "{name}" pular --agente {id}` |
|
|
249
|
+
| Before each step, each time it starts | `node _opencrew/core/scripts/estado.mjs "{name}" passo --n {K} --agente {id} --rotulo "{rótulo}" --mensagem "{frase}"` |
|
|
250
|
+
| Before asking the question of a checkpoint (instead of `passo`) | `node _opencrew/core/scripts/estado.mjs "{name}" checkpoint --n {K} --agente {id} --rotulo "{rótulo}"` |
|
|
251
|
+
| End of the run (After Pipeline Completion) | `node _opencrew/core/scripts/estado.mjs "{name}" concluir` |
|
|
252
|
+
| Run aborted after `iniciar`, by the user or by an error | `node _opencrew/core/scripts/estado.mjs "{name}" falhar --motivo "{motivo}"` |
|
|
253
|
+
|
|
254
|
+
- **One at a time** — Run these commands one at a time, waiting for the `ESTADO:` line of each
|
|
255
|
+
before the next — never in parallel or in the background (each one reads and rewrites the same file).
|
|
256
|
+
- **Values** — `{name}`: the crew code. `{N}`: how many steps will run, checkpoints included (a
|
|
257
|
+
deselected agent's steps do not count). `{K}`: the step's position among them, from 1. `{id}`: the agent's `id` column in
|
|
258
|
+
`crew-party.csv`; a step or checkpoint with no `agent:` goes without `--agente`
|
|
259
|
+
(the table shows the full form). `{rótulo}`: the step's name, in
|
|
260
|
+
a few words. `--mensagem` goes only when the agent changed since the last `passo` (so never on the first
|
|
261
|
+
one): one sentence on what the previous agent delivered — never look at the next step. `{motivo}`: why the run stopped.
|
|
262
|
+
- **Text on the command line** — `--rotulo`, `--mensagem` and `--motivo` go between double quotes,
|
|
263
|
+
on one line, starting with a letter or a digit, with only letters (accents included), digits,
|
|
264
|
+
spaces and `. , : ; - ( ) / ?`. Drop every other sign (quotes of any kind, `$`, backtick, `\`,
|
|
265
|
+
`%`, `!`, emoji). If no text is left, omit the option. Write them in the user's language.
|
|
266
|
+
- **After `iniciar`**, when it answers `ESTADO:OK`, show the user once:
|
|
267
|
+
`Escritório ligado. Se a página não estiver aberta, rode em outro terminal: node _opencrew/core/scripts/escritorio.mjs`
|
|
268
|
+
- **The Escritório never stops the run.** A command that fails, does not run or answers
|
|
269
|
+
`ESTADO:IGNORADO`: go on, do not repeat that event, ask nothing, and tell the user once per run,
|
|
270
|
+
in one line: `O escritório não foi atualizado nesta execução; o trabalho segue normalmente.` With
|
|
271
|
+
the reason "escritório desligado", say nothing and stop calling the script for the rest of this run.
|
|
272
|
+
- The script is the only writer: never read, write or describe `crews/{name}/state.json` yourself.
|
|
282
273
|
|
|
283
274
|
## Execution Rules
|
|
284
275
|
|
|
@@ -504,38 +495,15 @@ Apply this transformation consistently for every write in this step.
|
|
|
504
495
|
0. **Agent deselection check** — Read the step's `agent:` frontmatter field.
|
|
505
496
|
- If the step has an `agent:` value present AND it is in `skipped_agents` →
|
|
506
497
|
announce `⏭️ Skipping {Agent Name} (deselected for this run)` and skip this
|
|
507
|
-
step ENTIRELY: no
|
|
508
|
-
validation, no veto, no output file
|
|
498
|
+
step ENTIRELY: no Escritório command, no input validation, no execution, no output
|
|
499
|
+
validation, no veto, no output file. Advance to the next step in
|
|
509
500
|
`filtered_steps`.
|
|
510
501
|
- Checkpoints that declare `agent:` and whose agent was deselected are skipped the
|
|
511
502
|
same way. Checkpoints with no `agent:` field always run (backward compatible).
|
|
512
503
|
- When the selection step was skipped (no `agent_dependencies:`), `skipped_agents`
|
|
513
504
|
is empty → this check never fires (legacy behavior).
|
|
514
505
|
|
|
515
|
-
0b. **
|
|
516
|
-
```json
|
|
517
|
-
{
|
|
518
|
-
"crew": "{crew code from crew.yaml}",
|
|
519
|
-
"status": "running",
|
|
520
|
-
"step": {
|
|
521
|
-
"current": {1-based index of this step},
|
|
522
|
-
"total": {total steps in pipeline},
|
|
523
|
-
"label": "{step id or label}"
|
|
524
|
-
},
|
|
525
|
-
"agents": [
|
|
526
|
-
{
|
|
527
|
-
"id": "{agent id}",
|
|
528
|
-
"name": "{agent displayName}",
|
|
529
|
-
"icon": "{agent icon}",
|
|
530
|
-
"status": "{working if this is the current step's agent, done if already completed, skipped if in skipped_agents, idle otherwise}",
|
|
531
|
-
"desk": {preserve existing desk positions from state.json — do not change col/row}
|
|
532
|
-
}
|
|
533
|
-
],
|
|
534
|
-
"handoff": {preserve existing handoff object, or null if this is the first step},
|
|
535
|
-
"startedAt": "{ISO timestamp — set on the first step only, then preserve from existing state.json on subsequent steps}",
|
|
536
|
-
"updatedAt": "{ISO timestamp now}"
|
|
537
|
-
}
|
|
538
|
-
```
|
|
506
|
+
0b. **Escritório** — if it is on, run `passo`, or `checkpoint` when the step is a checkpoint (see "Escritório" above).
|
|
539
507
|
|
|
540
508
|
1. **Pre-Step Input Validation** — MANDATORY. If the step's frontmatter declares an `inputFile`, validate that the input exists before executing the step. Run via Bash tool:
|
|
541
509
|
```bash
|
|
@@ -756,48 +724,18 @@ When a step has `on_reject: {step-id}` (a review step):
|
|
|
756
724
|
text still contains `[PREENCHER: …]`, ask the user for each missing piece of real information
|
|
757
725
|
and write it into the text before approving.
|
|
758
726
|
|
|
759
|
-
### Dashboard Handoff (between steps)
|
|
760
|
-
|
|
761
|
-
Only if `dashboard_enabled` (otherwise skip this entire section). After a step
|
|
762
|
-
completes output and there IS a next step:
|
|
763
|
-
|
|
764
|
-
1. **Write delivering state** — Write `crews/{name}/state.json` with:
|
|
765
|
-
- Current step's agent: `"status": "delivering"`
|
|
766
|
-
- Next step's agent: `"status": "idle"`
|
|
767
|
-
- All other agents unchanged
|
|
768
|
-
- Pipeline `"status": "running"`
|
|
769
|
-
- Add or update `"handoff"`:
|
|
770
|
-
```json
|
|
771
|
-
"handoff": {
|
|
772
|
-
"from": "{current agent id}",
|
|
773
|
-
"to": "{next agent id}",
|
|
774
|
-
"message": "{one-sentence summary of what was produced, written in the user's language}",
|
|
775
|
-
"completedAt": "{ISO timestamp now}"
|
|
776
|
-
}
|
|
777
|
-
```
|
|
778
|
-
- `"updatedAt"`: now
|
|
779
|
-
|
|
780
|
-
2. _(No delay — proceed immediately to working state)_
|
|
781
|
-
|
|
782
|
-
2. **Write working state** — Write `crews/{name}/state.json` again with:
|
|
783
|
-
- Current agent: `"status": "done"`
|
|
784
|
-
- Next agent: `"status": "working"`
|
|
785
|
-
- Keep the `"handoff"` object from step 1 unchanged
|
|
786
|
-
- `"updatedAt"`: now
|
|
787
|
-
|
|
788
727
|
### Step Execution Order (Summary)
|
|
789
728
|
|
|
790
729
|
For reference, the complete execution order for each pipeline step is:
|
|
791
730
|
|
|
792
731
|
```
|
|
793
732
|
0. Agent deselection check (skip step if its agent was deselected)
|
|
794
|
-
0b.
|
|
733
|
+
0b. Escritório command (passo or checkpoint) — only if it is on
|
|
795
734
|
1. Pre-Step Input Validation (bash gate)
|
|
796
735
|
2. Read step file
|
|
797
736
|
3. Check execution mode and execute (subagent / inline / checkpoint)
|
|
798
737
|
4. Post-Step Output Validation (bash gate)
|
|
799
738
|
5. Veto Condition Enforcement
|
|
800
|
-
6. Dashboard Handoff (to next step) — only if dashboard_enabled
|
|
801
739
|
```
|
|
802
740
|
|
|
803
741
|
Steps 1 and 4 are binary bash gates. If either fails, the pipeline does NOT advance — the user is consulted.
|
|
@@ -806,31 +744,9 @@ Steps 1 and 4 are binary bash gates. If either fails, the pipeline does NOT adva
|
|
|
806
744
|
|
|
807
745
|
1. Save final output to `crews/{name}/output/{run_id}/{filename}.md`
|
|
808
746
|
(The run folder was created during initialization — no separate date subfolder needed)
|
|
809
|
-
1b. **
|
|
810
|
-
- `"status": "completed"`
|
|
811
|
-
- All agents: `"status": "done"`
|
|
812
|
-
- `"updatedAt"`: now
|
|
813
|
-
- `"completedAt"`: now
|
|
814
|
-
- `"startedAt"`: preserve from existing `state.json`
|
|
815
|
-
- Keep existing `"handoff"` object
|
|
816
|
-
|
|
817
|
-
### Post-Completion Cleanup (only if `dashboard_enabled`)
|
|
818
|
-
|
|
819
|
-
After writing the final "completed" state to `crews/{name}/state.json`:
|
|
820
|
-
|
|
821
|
-
1. Add the `completedAt` field (or `failedAt` if status is `failed`) with the current ISO timestamp
|
|
822
|
-
2. Copy `state.json` to the run output folder for permanent history:
|
|
823
|
-
```bash
|
|
824
|
-
cp "crews/{name}/state.json" "crews/{name}/output/{run_id}/state.json"
|
|
825
|
-
```
|
|
826
|
-
3. Leave the working copy of `crews/{name}/state.json` in place — do not delete it and
|
|
827
|
-
do not add an artificial delay. A dashboard watching the file already sees the
|
|
828
|
-
"completed" status the moment it's written; the next run's initialization (step 6)
|
|
829
|
-
overwrites this file from scratch. There is nothing to clean up.
|
|
830
|
-
|
|
831
|
-
This archives the run state for the `runs` command while keeping crew history available.
|
|
747
|
+
1b. **Escritório** — if it is on, run `concluir` (see "Escritório" above).
|
|
832
748
|
|
|
833
|
-
2. **Update crew memory** — write to BOTH files
|
|
749
|
+
2. **Update crew memory** — write to BOTH files:
|
|
834
750
|
|
|
835
751
|
### 2a. Update `memories.md` (living preferences)
|
|
836
752
|
|
|
@@ -947,6 +863,7 @@ This archives the run state for the `runs` command while keeping crew history av
|
|
|
947
863
|
- If a step file is missing, inform the user and suggest running `/opencrew edit {crew}` to fix.
|
|
948
864
|
- If company.md is empty, stop and redirect to onboarding.
|
|
949
865
|
- Never continue past a checkpoint without user input.
|
|
866
|
+
- When the run is aborted: if the Escritório is on, run `falhar` (see "Escritório" above).
|
|
950
867
|
|
|
951
868
|
## Pipeline State
|
|
952
869
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// O que o `/estado` do escritório devolve: o estado de cada crew, lido do disco a cada pedido.
|
|
2
|
+
// Só lê; quem grava `crews/<crew>/state.json` é o `estado.mjs`.
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const MARCA_DE_ORDEM = 0xfeff; // alguns editores e terminais gravam essa marca no início do arquivo
|
|
7
|
+
|
|
8
|
+
/** O estado de uma crew, ou null: arquivo ausente, pela metade, inválido ou sem `agents` em lista. */
|
|
9
|
+
async function lerCrew(raiz, crew) {
|
|
10
|
+
try {
|
|
11
|
+
const texto = await fs.readFile(path.join(raiz, 'crews', crew, 'state.json'), 'utf8');
|
|
12
|
+
const estado = JSON.parse(texto.charCodeAt(0) === MARCA_DE_ORDEM ? texto.slice(1) : texto);
|
|
13
|
+
return Array.isArray(estado?.agents) ? { crew, estado } : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `updatedAt` em milissegundos; ausente ou ilegível vale 0 (a crew vai para o fim). */
|
|
20
|
+
const atualizadaEm = ({ estado }) => Date.parse(estado.updatedAt) || 0;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* As crews de `crews/` que têm estado legível, a de `updatedAt` mais recente primeiro. Os nomes
|
|
24
|
+
* vêm da pasta, nunca do pedido. Sem a pasta `crews/`, ou sem nenhum estado: lista vazia.
|
|
25
|
+
* @returns {Promise<Array<{ crew: string, estado: object }>>}
|
|
26
|
+
*/
|
|
27
|
+
export async function lerCrews(raiz) {
|
|
28
|
+
const nomes = await fs.readdir(path.join(raiz, 'crews')).catch(() => []);
|
|
29
|
+
const lidas = await Promise.all(nomes.sort().map((nome) => lerCrew(raiz, nome)));
|
|
30
|
+
return lidas.filter(Boolean).sort((a, b) => atualizadaEm(b) - atualizadaEm(a));
|
|
31
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A porta do escritório: abrir, perguntar quem está numa porta ocupada e procurar a que serve.
|
|
2
|
+
// Spec: fase-e1-escritorio-ao-vivo.md, regra 15 (repositório do OpenCrew).
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
|
|
6
|
+
const LOCAL = '127.0.0.1';
|
|
7
|
+
const ULTIMA_PORTA = 65535;
|
|
8
|
+
// Quem responde na porta pode ser qualquer serviço: a resposta não é guardada sem limite.
|
|
9
|
+
const TETO_DA_SONDA = 1_000_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Alguém aceita conexão em 127.0.0.1:porta? No Windows, o `listen` em 127.0.0.1 abre por cima de
|
|
13
|
+
* quem escuta a mesma porta em 0.0.0.0 ou `::` e toma o tráfego local dele: só a conexão mostra
|
|
14
|
+
* que a porta tem dono. Porta livre recusa na hora; sem resposta dentro da espera, conta como livre.
|
|
15
|
+
*/
|
|
16
|
+
function alguemEscuta(porta, esperaMs = 1000) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const conexao = net.connect({ host: LOCAL, port: porta });
|
|
19
|
+
const fim = (escuta) => { conexao.destroy(); resolve(escuta); };
|
|
20
|
+
conexao.setTimeout(esperaMs, () => fim(false));
|
|
21
|
+
conexao.once('connect', () => fim(true)).once('error', () => fim(false));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Abre o servidor na porta, preso em 127.0.0.1.
|
|
27
|
+
* @returns {Promise<boolean>} true se subiu; false se a porta não pôde ser aberta (ocupada, por
|
|
28
|
+
* quem quer que seja e em qualquer endereço, ou reservada pelo sistema). O mesmo servidor pode
|
|
29
|
+
* tentar outra porta em seguida.
|
|
30
|
+
*/
|
|
31
|
+
export async function abrir(servidor, porta) {
|
|
32
|
+
if (porta !== 0 && await alguemEscuta(porta)) return false;
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const subiu = () => { servidor.off('error', falhou); resolve(true); };
|
|
35
|
+
const falhou = () => { servidor.off('listening', subiu); resolve(false); };
|
|
36
|
+
servidor.once('listening', subiu).once('error', falhou).listen(porta, LOCAL);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** O `projeto` de uma resposta do `/estado`, ou null quando a resposta não é a de um escritório. */
|
|
41
|
+
function projetoDaResposta(corpo) {
|
|
42
|
+
try {
|
|
43
|
+
const { projeto } = JSON.parse(corpo) ?? {};
|
|
44
|
+
return typeof projeto === 'string' ? projeto : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Junta o corpo da resposta e o entrega inteiro; cortada no meio ou acima do teto, entrega null. */
|
|
51
|
+
function juntar(res, entregar) {
|
|
52
|
+
let corpo = '';
|
|
53
|
+
res.setEncoding('utf8');
|
|
54
|
+
res.on('data', (parte) => {
|
|
55
|
+
corpo += parte;
|
|
56
|
+
if (corpo.length > TETO_DA_SONDA) entregar(null);
|
|
57
|
+
});
|
|
58
|
+
res.on('end', () => entregar(corpo));
|
|
59
|
+
res.on('error', () => entregar(null));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pergunta quem está na porta: `GET /estado`, com espera de até 1 s.
|
|
64
|
+
* @returns {Promise<string|null>} o `projeto` do escritório que respondeu; null para qualquer
|
|
65
|
+
* outra resposta (outro serviço, erro, resposta grande demais) ou nenhuma dentro da espera
|
|
66
|
+
*/
|
|
67
|
+
export function sondar(porta, esperaMs = 1000) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const pedido = http.get({ host: LOCAL, port: porta, path: '/estado', agent: false }, (res) => juntar(res, fim));
|
|
70
|
+
const relogio = setTimeout(() => fim(null), esperaMs);
|
|
71
|
+
pedido.on('error', () => fim(null));
|
|
72
|
+
function fim(corpo) {
|
|
73
|
+
clearTimeout(relogio);
|
|
74
|
+
pedido.destroy();
|
|
75
|
+
resolve(corpo === null ? null : projetoDaResposta(corpo));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Procura a porta do escritório, de `inicial` em diante, em até `tentativas` portas. Porta que
|
|
82
|
+
* não abre é sondada: se quem responde é o escritório deste projeto, não sobe outro.
|
|
83
|
+
* @param {object} o
|
|
84
|
+
* @param {number} o.inicial primeira porta
|
|
85
|
+
* @param {string} o.projeto id deste projeto
|
|
86
|
+
* @param {(porta: number) => Promise<boolean>} o.abrir tenta abrir o servidor na porta
|
|
87
|
+
* @param {(porta: number) => Promise<string|null>} o.sondar o `projeto` de quem está na porta
|
|
88
|
+
* @returns {Promise<{ tipo: 'aberto'|'ja-aberto'|'sem-porta', porta?: number, ultima: number }>}
|
|
89
|
+
* `ultima` é a última porta do intervalo (a que entra na mensagem "sem porta")
|
|
90
|
+
*/
|
|
91
|
+
export async function procurar({ inicial, projeto, abrir: tentar, sondar: perguntar, tentativas = 10 }) {
|
|
92
|
+
const ultima = Math.min(inicial + tentativas - 1, ULTIMA_PORTA);
|
|
93
|
+
for (let porta = inicial; porta <= ultima; porta++) {
|
|
94
|
+
if (await tentar(porta)) return { tipo: 'aberto', porta, ultima };
|
|
95
|
+
if ((await perguntar(porta)) === projeto) return { tipo: 'ja-aberto', porta, ultima };
|
|
96
|
+
}
|
|
97
|
+
return { tipo: 'sem-porta', ultima };
|
|
98
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Identidade do projeto para o escritório: diz se o servidor que responde numa porta é o deste
|
|
2
|
+
// projeto, sem expor o caminho da pasta.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 12 caracteres hexadecimais do SHA-256 do caminho real da raiz. No Windows o caminho vai para
|
|
9
|
+
* minúsculas antes: lá `D:\Projeto` e `d:\projeto` são a mesma pasta. Função pura.
|
|
10
|
+
*/
|
|
11
|
+
export function idDoProjeto(caminhoReal, plataforma = process.platform) {
|
|
12
|
+
const texto = plataforma === 'win32' ? caminhoReal.toLowerCase() : caminhoReal;
|
|
13
|
+
return createHash('sha256').update(texto).digest('hex').slice(0, 12);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Caminho real: o mesmo projeto aberto por um link de pasta dá o mesmo resultado. Onde o sistema
|
|
18
|
+
* não resolve (alguns discos virtuais), vale o caminho absoluto.
|
|
19
|
+
*/
|
|
20
|
+
function caminhoReal(raiz) {
|
|
21
|
+
try {
|
|
22
|
+
return realpathSync.native(raiz);
|
|
23
|
+
} catch {
|
|
24
|
+
return path.resolve(raiz);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** O id do projeto que mora em `raiz`. */
|
|
29
|
+
export const projetoDe = (raiz) => idDoProjeto(caminhoReal(raiz));
|