@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +50 -5
  3. package/package.json +1 -1
  4. package/templates/AGENTS.md +20 -6
  5. package/templates/_opencrew/.opencrew-version +1 -1
  6. package/templates/_opencrew/core/escritorio/animacao.js +64 -0
  7. package/templates/_opencrew/core/escritorio/app.js +137 -0
  8. package/templates/_opencrew/core/escritorio/cena.js +132 -0
  9. package/templates/_opencrew/core/escritorio/demo.js +79 -0
  10. package/templates/_opencrew/core/escritorio/escala.js +27 -0
  11. package/templates/_opencrew/core/escritorio/index.html +166 -0
  12. package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
  13. package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
  14. package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
  15. package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
  16. package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
  17. package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
  18. package/templates/_opencrew/core/escritorio/modelo.js +29 -0
  19. package/templates/_opencrew/core/escritorio/painel.js +120 -0
  20. package/templates/_opencrew/core/escritorio/quadro.js +106 -0
  21. package/templates/_opencrew/core/escritorio/rota.js +62 -0
  22. package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
  23. package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
  24. package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
  25. package/templates/_opencrew/core/escritorio/sprites.js +187 -0
  26. package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
  27. package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
  28. package/templates/_opencrew/core/runner.pipeline.md +45 -128
  29. package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
  30. package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
  31. package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
  32. package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
  33. package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
  34. package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
  35. package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
  36. package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
  37. package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
  38. package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
  39. package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
  40. 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 `state.json`, in "🤖 {name} is working…" announcements, and
135
- in the dashboard. If `displayName` is missing, empty, or set to the role/title instead
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 dashboard and the
5
- Pipeline Runner announce "Pesquisador" instead of "Pedro Pesquisa").
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: Refresh `state.json` (only if it exists)
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. **Check the Dashboard toggle** — the visual dashboard (`state.json` writes) is an
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. **Initialize state.json** (only if `dashboard_enabled` — see step 1a; otherwise skip this entire step, including all sub-steps below):
248
- - **IMPORTANT**: When enabled, write to `crews/{name}/state.json` before every step and after every handoff, as described throughout this document. When `dashboard_enabled` is false, never create, write, or delete this file.
249
- - Create `state.json` from scratch:
250
- a. Read `crews/{name}/crew-party.csv` — for each agent row (skip header), extract:
251
- - `id`: take the `path` column, strip `./agents/` prefix and `.agent.md` suffix
252
- (e.g. `./agents/researcher.agent.md` → `researcher`)
253
- - `name`: use the `displayName` column
254
- - `icon`: use the `icon` column
255
- b. Assign desk positions by agent order (0-based index):
256
- - `col = (index % 3) + 1`
257
- - `row = floor(index / 3) + 1`
258
- (index 0 → col:1 row:1, index 1 → col:2 row:1, index 2 → col:3 row:1, index 3 → col:1 row:2, etc.)
259
- c. Read `crews/{name}/crew.yaml` — count items in `pipeline.steps` for `total`
260
- d. Write `crews/{name}/state.json` with the Write tool:
261
- ```json
262
- {
263
- "crew": "{crew code from crew.yaml}",
264
- "status": "idle",
265
- "step": { "current": 0, "total": {step count from c}, "label": "" },
266
- "agents": [
267
- {
268
- "id": "{agent id}",
269
- "name": "{agent displayName}",
270
- "icon": "{agent icon}",
271
- "status": "idle",
272
- "desk": { "col": {col from b}, "row": {row from b} }
273
- }
274
- ],
275
- "handoff": null,
276
- "startedAt": null,
277
- "updatedAt": "{ISO timestamp now}"
278
- }
279
- ```
280
- Include one entry per agent, in crew-party.csv order. For each agent, set
281
- `"status"` to `"skipped"` if it is in `skipped_agents`, otherwise `"idle"`.
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 dashboard update, no input validation, no execution, no output
508
- validation, no veto, no output file, no handoff. Advance to the next step in
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. **Update dashboard** (only if `dashboard_enabled`; otherwise skip to step 1). Write `crews/{name}/state.json` using the Write tool. Use this content:
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. Dashboard update (state.json) — only if dashboard_enabled
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. **Update dashboard** (only if `dashboard_enabled`; otherwise skip to step 2 below). Write `crews/{name}/state.json` with:
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 (runs after Post-Completion Cleanup above):
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));