@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,57 @@
3
3
  All notable changes to opencrew are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## [1.7.0] — 2026-10-06
7
+
8
+ Fase E1 "Escritório ao vivo — a equipe trabalhando, em 8 bits"
9
+ (`specs/fase-e1-escritorio-ao-vivo.md`). Chega a quem já usa com um
10
+ `npx @aksp/opencrew@latest update`.
11
+
12
+ ### Added
13
+ - **Escritório ao vivo.** Uma página em pixel-art, aberta no navegador, mostra a crew
14
+ trabalhando: cada agente na sua mesa, digitando na vez dele, levando o papel ao colega na
15
+ passagem de bastão, de mão levantada quando espera a sua resposta, com ✓ quando termina e "!"
16
+ quando falha. Ao lado, o passo atual, a lista dos agentes com o status por extenso e o que cada
17
+ um fez, e a última passagem de bastão. O título da aba acompanha a execução.
18
+ - **`/opencrew dashboard`** liga o Escritório, sobe a página e mostra o endereço
19
+ (`http://127.0.0.1:4747`, ou a porta livre seguinte); repetir o comando devolve o mesmo
20
+ endereço. **`/opencrew dashboard off`** desliga. Continua desligado por padrão.
21
+ - Roda só no seu computador, sem internet e sem medição: o servidor
22
+ (`_opencrew/core/scripts/escritorio.mjs`) escuta só em `127.0.0.1`, só lê e não escreve em disco.
23
+ - Sem execução nenhuma, a página roda uma demonstração e troca sozinha para a execução real
24
+ quando ela aparece (`?demo` no endereço força a demonstração). Com mais de uma crew, mostra a
25
+ de atualização mais recente e um seletor com as outras.
26
+ - A página não mente sobre o que não sabe: execução há mais de 2 minutos sem novidade mostra há
27
+ quanto tempo foi a última atualização; há mais de 20, o agente sai da pose de digitar e aparece
28
+ "sem sinal". Servidor fora do ar: a página avisa e tenta de novo sozinha.
29
+
30
+ ### Changed
31
+ - **Quem avisa o Escritório é um script, não a IA escrevendo JSON.** Com o Escritório ligado, o
32
+ runner roda um comando curto por passo (`_opencrew/core/scripts/estado.mjs`). Checkpoint, agente
33
+ pulado e execução que falha passam a aparecer; antes nunca eram gravados. Falha desse comando
34
+ não para a execução: o runner avisa uma vez e segue.
35
+ - **Com o Escritório ligado, o estado final deixa de ser copiado para
36
+ `crews/<crew>/output/<run>/state.json`.** O estado da execução mora só em
37
+ `crews/<crew>/state.json`.
38
+ - `state.json`: os agentes ganham os status `checkpoint` e `failed` e o campo `label`; a
39
+ execução ganha `checkpoint` e `failed`; `desk` e `delivering` deixam de ser gravados. Arquivo
40
+ gravado por versão anterior continua sendo lido pela página.
41
+ - Os prompts de criar e de consertar crew não escrevem mais `state.json`.
42
+ - README: a nota "o dashboard não é instalado" deu lugar à seção "Escritório ao vivo".
43
+
44
+ ### Removed
45
+ - A pasta `dashboard/` do repositório (o desenho antigo, que nunca foi instalado pelo `init`). O
46
+ que servia migrou para `_opencrew/core/escritorio/`, sem os defeitos apontados na auditoria:
47
+ nome de agente entrava na página como HTML, o modo ao vivo lia o arquivo no lugar errado e a
48
+ página congelava se a primeira leitura falhasse.
49
+
50
+ ### Internal
51
+ - `npm run verify`: o lint passa a cobrir `templates/_opencrew/core/escritorio/` (com os nomes
52
+ globais de navegador) e o alerta de tamanho mede os `.js` dessa pasta. Travas novas:
53
+ `tests/estado*.test.js`, `tests/escritorio*.test.js`, `tests/runtime-contracts-e1.test.js`,
54
+ `tests/docs-e1.test.js` e os cenários E1-07a, E1-07c e E1-upg nos testes de pacote, de
55
+ referências e de upgrade. A trava de conteúdo do mantenedor passa a ler também `.mjs` e `.css`.
56
+
6
57
  ## [1.6.3] — 2026-10-06
7
58
 
8
59
  **O Node mínimo subiu para o 20.17, numa versão de correção.** O pacote dizia 20.0, mas a lista
package/README.md CHANGED
@@ -40,6 +40,9 @@ 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
+ - 🖥️ **Escritório ao vivo** — veja a equipe trabalhando numa sala em pixel-art, no navegador:
44
+ quem está digitando, quem passou o bastão, quem espera a sua resposta. Opcional, desligado por
45
+ padrão, só neste computador. Liga com `/opencrew dashboard`.
43
46
  - 📂 **Crew que conhece o projeto** — liste em `fontes:` os arquivos e pastas do seu projeto
44
47
  (decisões, calendário, manual de marca) e a crew os lê em todo run, tratando-os como verdade.
45
48
  Reorganizou as pastas? No início do run ela confere os caminhos, acha para onde o arquivo foi
@@ -175,6 +178,8 @@ meu-projeto/
175
178
  │ │ ├── skills.engine.md ← gerenciador de skills
176
179
  │ │ ├── architect.agent.yaml ← definição do Arquiteto
177
180
  │ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
181
+ │ │ ├── scripts/ ← verificador, conferência de fontes e os scripts do Escritório
182
+ │ │ ├── escritorio/ ← página do Escritório ao vivo (abre com /opencrew dashboard)
178
183
  │ │ └── prompts/ ← 13 prompts de fase (discovery, design, build, etc.)
179
184
  │ ├── agents/ ← 5 agentes base compartilhados
180
185
  │ │ ├── researcher.agent.md
@@ -184,7 +189,7 @@ meu-projeto/
184
189
  │ │ └── strategist.agent.md
185
190
  │ ├── _memory/
186
191
  │ │ ├── company.md ← perfil da sua empresa (onboarding)
187
- │ │ └── preferences.md ← idioma, tier padrão, dashboard
192
+ │ │ └── preferences.md ← idioma, tier padrão, Escritório ligado ou desligado
188
193
  │ └── .opencrew-version
189
194
  │
190
195
  ├── crews/ ← suas crews vivem aqui
@@ -202,9 +207,47 @@ meu-projeto/
202
207
  │ └── ...
203
208
  ```
204
209
 
205
- > O dashboard visual (`dashboard/index.html`) **não é instalado** pelo `init` — ele vive
206
- > só no repositório do OpenCrew e ainda é experimental (decisão de publicar ou remover:
207
- > fase U3a — ver `IDEIAS.md` no repositório).
210
+ ---
211
+
212
+ ## Escritório ao vivo
213
+
214
+ Quer ver a equipe trabalhando? O **Escritório** é uma página em pixel-art, aberta no navegador,
215
+ em que cada agente tem a sua mesa: digita quando é a vez dele, leva o papel ao colega na passagem
216
+ de bastão e levanta a mão quando espera uma resposta sua. Ao lado do desenho ficam o passo atual,
217
+ a lista dos agentes (com o status por extenso e o que cada um fez) e a última passagem de bastão.
218
+
219
+ **Como abrir:** no chat da sua IDE, digite `/opencrew dashboard`. O comando liga o Escritório,
220
+ sobe a página e mostra o endereço — `http://127.0.0.1:4747`, ou a porta livre seguinte. A próxima
221
+ execução de crew aparece ali; enquanto não há nenhuma, a página roda uma demonstração. Repetir o
222
+ comando é seguro: ele devolve o mesmo endereço.
223
+
224
+ Se a sua IDE não roda comando em segundo plano, ela mostra o comando para você rodar em outro
225
+ terminal, na pasta do projeto:
226
+
227
+ ```bash
228
+ node _opencrew/core/scripts/escritorio.mjs # Ctrl+C para fechar
229
+ node _opencrew/core/scripts/escritorio.mjs --porta 5000
230
+ ```
231
+
232
+ Para desligar: `/opencrew dashboard off`.
233
+
234
+ O que vale saber antes de ligar:
235
+
236
+ - **Vem desligado.** Sem o `/opencrew dashboard`, nada muda nas suas execuções.
237
+ - **Roda só neste computador, sem internet e sem medição.** A página é servida em `127.0.0.1`,
238
+ só lê o estado das suas crews (`crews/<crew>/state.json`), não carrega nada de fora e não envia
239
+ dado nenhum para lugar nenhum.
240
+ - **Com ele ligado, cada passo custa um comando curto a mais.** É assim que a IA avisa o que está
241
+ fazendo: um comando de terminal por passo. Em IDE que pede aprovação a cada comando, libere o
242
+ `estado.mjs` uma vez.
243
+ - **A tela mostra o que a IA avisa, e pode atrasar.** Se a IA pular um aviso, o desenho só se
244
+ acerta no passo seguinte. Depois de 2 minutos sem novidade a página diz há quanto tempo foi a
245
+ última atualização; depois de 20, o agente aparece "sem sinal" — o que não quer dizer que
246
+ travou: um passo longo é normal.
247
+ - **O Escritório nunca para a execução.** Se o aviso falhar, a crew segue trabalhando.
248
+ - **Até 12 mesas.** Numa crew maior, os agentes a mais aparecem só na lista ao lado.
249
+
250
+ Quem já usa o OpenCrew recebe o Escritório com um `npx @aksp/opencrew@latest update`.
208
251
 
209
252
  ---
210
253
 
@@ -284,7 +327,9 @@ npx @aksp/opencrew update --check
284
327
  | `/opencrew delete <nome>` | Remove uma crew |
285
328
  | `/opencrew skills` | Navega, instala ou remove skills |
286
329
  | `/opencrew install <skill>` | Instala uma skill do catálogo |
287
- | `/opencrew settings` | Altera preferências (idioma, tier, dashboard) |
330
+ | `/opencrew settings` | Altera preferências (idioma, tier, Escritório) |
331
+ | `/opencrew dashboard` | Liga e abre o Escritório ao vivo (a equipe trabalhando, no navegador) |
332
+ | `/opencrew dashboard off` | Desliga o Escritório |
288
333
  | `/opencrew show-company` | Mostra o perfil da empresa |
289
334
  | `/opencrew edit-company` | Reconfigura o perfil da empresa |
290
335
  | `/opencrew help` | Mostra a lista de comandos |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aksp/opencrew",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -58,6 +58,8 @@ Route input to the matching action:
58
58
  | `/opencrew edit-company` | Re-run company profile setup |
59
59
  | `/opencrew show-company` | Display company.md contents |
60
60
  | `/opencrew settings` | Show/edit preferences.md |
61
+ | `/opencrew dashboard` | Turn on and open the Escritório (live view) — see "Dashboard (Optional)" |
62
+ | `/opencrew dashboard off` | Turn the Escritório off — see "Dashboard (Optional)" |
61
63
  | `/opencrew reset` | Confirm and reset all configuration |
62
64
  | Natural language about crews | Infer intent and route accordingly |
63
65
 
@@ -90,12 +92,24 @@ When running a crew:
90
92
 
91
93
  ## Dashboard (Optional)
92
94
 
93
- The dashboard is an optional animated view of a crew run (`dashboard/index.html`).
94
- It is **disabled by default**; most installs never use it. Toggle it via
95
- `Dashboard: enabled|disabled` in `_opencrew/_memory/preferences.md` (editable via
96
- `/opencrew settings`). When disabled, the runner never writes `state.json`; when
97
- enabled, it writes `crews/{name}/state.json` before each step and at every handoff
98
- (see `_opencrew/core/runner.pipeline.md`).
95
+ The dashboard is the **Escritório**: a local page that shows the crew at work, step by step.
96
+ It is **disabled by default**. The switch is the `Dashboard` line of
97
+ `_opencrew/_memory/preferences.md`; while it says `enabled`, the Pipeline Runner reports each
98
+ step with one short command (see `_opencrew/core/runner.pipeline.md`).
99
+
100
+ **`/opencrew dashboard`** — in this order:
101
+ 1. Write `- **Dashboard:** enabled` in `_opencrew/_memory/preferences.md`, changing only that
102
+ line (if the file has no `Dashboard` line, add it at the end). Leave the rest of the file as is.
103
+ 2. Start `node _opencrew/core/scripts/escritorio.mjs` in the background, from the project root.
104
+ The line it prints carries the address (`http://127.0.0.1:<port>`; the port may vary).
105
+ 3. Show the address and tell the user that the next crew run appears there.
106
+
107
+ If your IDE cannot keep a process running in the background, do step 1 and show the user the
108
+ command of step 2 to run in another terminal. Running `/opencrew dashboard` again is safe: the
109
+ script answers with the same address instead of opening a second page.
110
+
111
+ **`/opencrew dashboard off`** — write `- **Dashboard:** disabled` the same way (only that line)
112
+ and touch nothing else: stop no process, delete no file.
99
113
 
100
114
  ## Language Handling
101
115
 
@@ -1 +1 @@
1
- 1.6.3
1
+ 1.7.0
@@ -0,0 +1,64 @@
1
+ // O que anima (regra 28): a passagem de bastão e a comemoração nascem da diferença entre duas
2
+ // leituras da mesma crew, nunca de um status gravado. `transicao` anota no modelo a hora em que
3
+ // cada uma começou; `quadro` (em quadro.js) desenha pelo tempo decorrido desde então.
4
+ // Puro: sem DOM, sem relógio, sem rede. A hora entra por parâmetro.
5
+ // Spec: fase-e1-escritorio-ao-vivo.md, regra 28 (repositório do OpenCrew).
6
+
7
+ /** A entrega: ida até a frente da mesa de quem recebe, com o papel, e volta até sentar. */
8
+ export const IDA_MS = 2000;
9
+ export const VOLTA_MS = 2000;
10
+ /** Quanto dura a comemoração de uma execução concluída. */
11
+ export const COMEMORACAO_MS = 4000;
12
+ /** Quem digita troca de pose a cada 0,3 s; quem anda, a cada 0,15 s. */
13
+ export const DIGITAR_MS = 300;
14
+ export const ANDAR_MS = 150;
15
+
16
+ /** Nada em curso: é o que vale na primeira leitura de cada crew. */
17
+ export const SEM_ANIMACAO = Object.freeze({ entrega: null, comemoracao: null });
18
+
19
+ /** As duas leituras têm estado e são da mesma crew (ou as duas são da demonstração). */
20
+ function mesmaCrew(anterior, atual) {
21
+ if (!anterior || !atual || anterior.semEstado || atual.semEstado) return false;
22
+ return Boolean(anterior.demo) === Boolean(atual.demo) && anterior.crew === atual.crew;
23
+ }
24
+
25
+ /** A passagem de bastão cuja data a leitura anterior não trazia; senão (ou sem data), `null`. */
26
+ function entregaNova(anterior, atual, agoraMs) {
27
+ const { passagem } = atual;
28
+ if (!passagem || passagem.completedAt === (anterior.passagem?.completedAt ?? '')) return null;
29
+ return { deId: passagem.deId, paraId: passagem.paraId, inicioMs: agoraMs };
30
+ }
31
+
32
+ /** Começa quando a execução passa a `completed`, segue enquanto ela continuar assim. */
33
+ function comemoracaoDe(anterior, atual, agoraMs) {
34
+ if (atual.execucao !== 'completed') return null;
35
+ return anterior.execucao === 'completed' ? (anterior.animacao?.comemoracao ?? null) : { inicioMs: agoraMs };
36
+ }
37
+
38
+ /**
39
+ * Compara duas leituras e devolve a atual com o que está animando. A página chama a cada leitura
40
+ * (cada consulta ao servidor, cada passo da demonstração), e não só quando vai pintar: assim a
41
+ * entrega que acontece com a aba oculta já aparece terminada na volta.
42
+ * @param {object|null} anterior o que esta função devolveu na leitura anterior; `null` na primeira
43
+ * @param {object} atual o modelo da leitura atual (de `montarPagina` ou `montarModelo`)
44
+ * @param {number} agoraMs a hora desta leitura, em milissegundos, no mesmo relógio de `quadro`
45
+ * @returns {object} `atual` mais `animacao`: `{ entrega, comemoracao }` · `entrega`:
46
+ * `{ deId, paraId, inicioMs }` desde que `passagem.completedAt` mudou entre duas leituras da
47
+ * mesma crew, ou `null`; uma passagem nova substitui a que está em curso, e qualquer outra
48
+ * mudança de estado a deixa como está · `comemoracao`: `{ inicioMs }` desde que a execução
49
+ * passou a `completed`, ou `null` · na primeira leitura de uma crew (abrir, trocar de crew,
50
+ * sair da demonstração, voltar de "sem estado"), as duas são `null` · `animacao` só guarda
51
+ * quando cada uma começou, e a entrega fica anotada depois de terminar: quem diz o que ainda
52
+ * se move num instante é `quadro`
53
+ */
54
+ export function transicao(anterior, atual, agoraMs) {
55
+ if (!mesmaCrew(anterior, atual)) return { ...atual, animacao: SEM_ANIMACAO };
56
+ const emCurso = anterior.animacao?.entrega ?? null;
57
+ return {
58
+ ...atual,
59
+ animacao: {
60
+ entrega: entregaNova(anterior, atual, agoraMs) ?? emCurso,
61
+ comemoracao: comemoracaoDe(anterior, atual, agoraMs),
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,137 @@
1
+ // A página do escritório: pergunta o estado ao servidor a cada segundo (e na hora, ao voltar para
2
+ // a aba), entrega a resposta ao modelo e aplica o que ele devolve — o desenho no canvas, os
3
+ // rótulos por cima e o painel em volta. Consulta que falha mantém o último estado bom e liga o
4
+ // aviso; a página segue tentando, sem recarregar.
5
+ // É o único módulo que toca na janela: o relógio, a rede, o endereço (`?demo`), o tamanho da tela
6
+ // e a preferência "reduzir movimento" entram no modelo por parâmetro.
7
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 20 a 23 e 28 (repositório do OpenCrew).
8
+ import { ALTURA, LARGURA, MEMORIA_INICIAL, consultar, emExibicao, escala, escolher, montarPagina, quadro, transicao } from './modelo.js';
9
+ import { animar } from './cena.js';
10
+ import { criarPainel } from './painel.js';
11
+ import { criarRotulos } from './rotulos.js';
12
+
13
+ /** De quanto em quanto tempo a página pergunta o estado. */
14
+ const INTERVALO_MS = 1000;
15
+ /** Consulta sem resposta neste prazo é abandonada e conta como falha: a seguinte sai no ritmo normal. */
16
+ const PRAZO_MS = 4000;
17
+ /** Na demonstração nenhuma consulta traz o passo seguinte: o roteiro é relido neste ritmo. */
18
+ const RITMO_DA_DEMO_MS = 200;
19
+ /** Relativo à página: o servidor só atende o próprio endereço. */
20
+ const ENDERECO = 'estado';
21
+
22
+ /** Regra 16: o canvas tem 320·N × 180·N pixels de verdade e, na página, o tamanho que `escala` devolve. */
23
+ function ajustar(p) {
24
+ const [moldura, sala, tela] = ['moldura', 'sala', 'tela'].map(p.el);
25
+ p.razao = p.janela.devicePixelRatio;
26
+ const { n, largura, altura } = escala(moldura.clientWidth, moldura.clientHeight, p.razao);
27
+ if (tela.width !== LARGURA * n) Object.assign(tela, { width: LARGURA * n, height: ALTURA * n });
28
+ sala.style.width = `${largura}px`;
29
+ sala.style.height = `${altura}px`;
30
+ // Quantos pixels da página vale um pixel lógico: o CSS dos rótulos acompanha a escala por aqui.
31
+ sala.style.setProperty('--u', String(largura / LARGURA));
32
+ }
33
+
34
+ /**
35
+ * Uma leitura: monta o modelo deste instante, compara com a leitura anterior (é daí que saem a
36
+ * entrega e a comemoração) e escreve a página. Na primeira, mostra a página e mede o canvas.
37
+ */
38
+ function ler(p) {
39
+ const agora = Date.now();
40
+ const naDemo = emExibicao(p.memoria, { demo: p.demo }).modo === 'demo';
41
+ p.demoDesde = naDemo ? (p.demoDesde ?? agora) : null;
42
+ const opcoes = { agoraMs: agora, reduzirMovimento: p.menosMovimento.matches, demo: p.demo, decorridoDemoMs: naDemo ? agora - p.demoDesde : 0 };
43
+ const primeira = p.leitura === null;
44
+ p.leitura = transicao(p.leitura, montarPagina(p.memoria, opcoes), agora);
45
+ p.lidaEm = agora;
46
+ p.painel.atualizar(p.leitura);
47
+ p.rotulos.atualizar(p.leitura.agentes);
48
+ if (!primeira) return;
49
+ p.el('pagina').hidden = false;
50
+ ajustar(p);
51
+ }
52
+
53
+ /** A resposta de `/estado`, ou `null` se a consulta falhou, demorou demais ou veio inválida. */
54
+ async function perguntar(p) {
55
+ const corte = new AbortController();
56
+ const prazo = p.janela.setTimeout(() => corte.abort(), PRAZO_MS);
57
+ try {
58
+ const resposta = await p.janela.fetch(ENDERECO, { cache: 'no-store', signal: corte.signal });
59
+ return resposta.ok ? await resposta.json() : null;
60
+ } catch {
61
+ return null;
62
+ } finally {
63
+ p.janela.clearTimeout(prazo);
64
+ }
65
+ }
66
+
67
+ /** Regra 20: uma consulta por vez; a que falha entra na memória como falha, e a página não para. */
68
+ async function consultarServidor(p) {
69
+ if (p.consultando) return;
70
+ p.consultando = true;
71
+ const resposta = await perguntar(p);
72
+ p.consultando = false;
73
+ p.memoria = consultar(p.memoria ?? MEMORIA_INICIAL, resposta);
74
+ ler(p);
75
+ }
76
+
77
+ /** Regra 22: a escolha do usuário no seletor vale até ele trocar. */
78
+ function escolherCrew(p, crew) {
79
+ if (!p.memoria) return;
80
+ p.memoria = escolher(p.memoria, crew);
81
+ ler(p);
82
+ }
83
+
84
+ /** Os bonecos deste instante, para a cena pintar; `null` antes da primeira leitura. */
85
+ function quadroAgora(p) {
86
+ if (!p.leitura) return null;
87
+ const agora = Date.now();
88
+ if (p.leitura.modo === 'demo' && agora - p.lidaEm >= RITMO_DA_DEMO_MS) ler(p);
89
+ if (p.janela.devicePixelRatio !== p.razao) ajustar(p);
90
+ const bonecos = quadro(p.leitura, agora);
91
+ p.rotulos.acompanhar(bonecos);
92
+ return bonecos;
93
+ }
94
+
95
+ /** O que a página guarda entre uma consulta e a seguinte. */
96
+ function criarPagina(janela) {
97
+ const documento = janela.document;
98
+ const el = (id) => documento.getElementById(id);
99
+ const p = {
100
+ janela,
101
+ documento,
102
+ el,
103
+ demo: new URLSearchParams(janela.location.search).has('demo'),
104
+ menosMovimento: janela.matchMedia('(prefers-reduced-motion: reduce)'),
105
+ rotulos: criarRotulos(el('rotulos'), documento),
106
+ memoria: null, // as consultas ao servidor; `null` até a primeira voltar
107
+ leitura: null, // o modelo da última leitura, com o que está animando
108
+ lidaEm: 0,
109
+ demoDesde: null, // quando a demonstração entrou na tela
110
+ consultando: false,
111
+ razao: 0, // a razão de pixels com que o canvas foi medido
112
+ };
113
+ p.painel = criarPainel(documento, (crew) => escolherCrew(p, crew));
114
+ return p;
115
+ }
116
+
117
+ /**
118
+ * Liga a página a uma janela. A frase de abertura, que já está no HTML, só some depois do
119
+ * primeiro desenho: se algo aqui não carregar ou não rodar, ela continua na tela.
120
+ * @param {Window} janela a janela do navegador (nos testes, uma de mentira)
121
+ */
122
+ export function iniciar(janela) {
123
+ const p = criarPagina(janela);
124
+ const reajustar = () => ajustar(p);
125
+ janela.addEventListener('resize', reajustar);
126
+ if (janela.ResizeObserver) new janela.ResizeObserver(reajustar).observe(p.el('moldura'));
127
+ p.documento.addEventListener('visibilitychange', () => {
128
+ if (!p.documento.hidden) consultarServidor(p);
129
+ });
130
+ janela.setInterval(() => consultarServidor(p), INTERVALO_MS);
131
+ animar(janela, p.el('tela'), () => quadroAgora(p), () => {
132
+ p.el('abertura').hidden = true;
133
+ });
134
+ consultarServidor(p);
135
+ }
136
+
137
+ if (typeof window !== 'undefined') iniciar(window);
@@ -0,0 +1,132 @@
1
+ // A cena: pinta no canvas, em pixel lógico (320×180), o quadro que o modelo calculou — a sala, a
2
+ // mesa de cada agente do elenco com o monitor, e os bonecos. Nada aqui decide pose, posição ou
3
+ // cor de monitor: isso chega pronto de `quadro`. A cena só preenche retângulos; nenhum texto
4
+ // entra no canvas (nome e balão são elementos da página, numa camada por cima).
5
+ // O laço de animação pede um quadro por vez ao navegador e só pinta de novo quando ele muda.
6
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 18, 19 e 28 (repositório do OpenCrew).
7
+ import { ALTURA, BONECO, COLUNAS, LARGURA, LARGURA_COLUNA, TOPO } from './modelo.js';
8
+ import { coresDoBoneco, linhasDoBoneco, retangulos } from './sprites.js';
9
+ import { JANELA, LADRILHO, LADRILHO_ESCURO, PAREDE, PLANTA, RELOGIO } from './sprites-sala.js';
10
+ import {
11
+ MESA, MESA_ACIMA, MONITOR, MONITOR_EM, SINAL_DUVIDA, SINAL_FALHA, SINAL_FEITO, SOMBRA,
12
+ TELA_AMBAR, TELA_EM, TELA_VERMELHA, TELAS_ACESAS,
13
+ } from './sprites-mesa.js';
14
+
15
+ /** Quem foi pulado aparece meio transparente. */
16
+ const ALFA_DO_PULADO = 0.45;
17
+ /** O sinal sobre a cabeça, por ação; quem entrega ou comemora não tem sinal. */
18
+ const SINAIS = Object.freeze({ done: SINAL_FEITO, failed: SINAL_FALHA, 'sem-sinal': SINAL_DUVIDA });
19
+ /** A tela do monitor, pela cor que o quadro manda pintar agora; apagado é o próprio monitor. */
20
+ const TELAS = Object.freeze({
21
+ aceso: (fase) => TELAS_ACESAS[fase % TELAS_ACESAS.length],
22
+ ambar: () => TELA_AMBAR,
23
+ vermelho: () => TELA_VERMELHA,
24
+ });
25
+
26
+ const largura = (desenho) => desenho.linhas[0].length;
27
+ const altura = (desenho) => desenho.linhas.length;
28
+ const meio = (coluna) => coluna * LARGURA_COLUNA + LARGURA_COLUNA / 2;
29
+ const centrado = (desenho, x, y) => ({ desenho, x: x - Math.floor(largura(desenho) / 2), y });
30
+
31
+ // Na parede, uma janela sobre cada coluna de mesas e o relógio no meio; no chão, encostadas na
32
+ // parede e fora do caminho de quem anda, duas plantas.
33
+ const DECORACAO = Object.freeze([
34
+ ...Array.from({ length: COLUNAS }, (_, coluna) => centrado(JANELA, meio(coluna), 3)),
35
+ centrado(RELOGIO, LARGURA / 2, 3),
36
+ centrado(PLANTA, LARGURA_COLUNA, 6),
37
+ centrado(PLANTA, LARGURA - LARGURA_COLUNA, 6),
38
+ ]);
39
+
40
+ /** Preenche uma matriz de pixels com o canto superior esquerdo em (x, y). */
41
+ function pintar(ctx, { paleta, linhas }, x, y) {
42
+ for (const [dx, dy, l, a, letra] of retangulos(linhas)) {
43
+ ctx.fillStyle = paleta[letra];
44
+ ctx.fillRect(x + dx, y + dy, l, a);
45
+ }
46
+ }
47
+
48
+ /** O piso: ladrilhos claros e escuros em xadrez, da parede até o fim da tela. */
49
+ function pintarPiso(ctx) {
50
+ for (let y = TOPO, linha = 0; y < ALTURA; y += altura(LADRILHO), linha++) {
51
+ for (let x = 0, coluna = 0; x < LARGURA; x += largura(LADRILHO), coluna++) {
52
+ pintar(ctx, (linha + coluna) % 2 ? LADRILHO_ESCURO : LADRILHO, x, y);
53
+ }
54
+ }
55
+ }
56
+
57
+ function pintarSala(ctx) {
58
+ for (let x = 0; x < LARGURA; x += largura(PAREDE)) pintar(ctx, PAREDE, x, 0);
59
+ pintarPiso(ctx);
60
+ for (const { desenho, x, y } of DECORACAO) pintar(ctx, desenho, x, y);
61
+ }
62
+
63
+ /** A mesa de um agente, com o monitor na cor e na fase que o quadro traz. */
64
+ function pintarMesa(ctx, { mesa, monitor, monitorFase }) {
65
+ const [x, y] = [mesa.retangulo.x + MONITOR_EM.x, mesa.retangulo.y + MONITOR_EM.y];
66
+ pintar(ctx, MESA, mesa.retangulo.x, mesa.retangulo.y - MESA_ACIMA);
67
+ pintar(ctx, MONITOR, x, y);
68
+ const tela = TELAS[monitor]?.(monitorFase);
69
+ if (tela) pintar(ctx, tela, x + TELA_EM.x, y + TELA_EM.y);
70
+ }
71
+
72
+ /** O boneco na pose do quadro, com a sombra no chão (se está em pé) e o sinal sobre a cabeça. */
73
+ function pintarBoneco(ctx, boneco) {
74
+ const { x, y, acao, pose, papel, espelhado } = boneco;
75
+ const linhas = linhasDoBoneco(pose, { papel, espelhado, festa: acao === 'comemorando' && pose === 'mao' });
76
+ const sinal = SINAIS[acao];
77
+ if (!boneco.sentado) pintar(ctx, SOMBRA, x + Math.floor((BONECO - largura(SOMBRA)) / 2), y + BONECO);
78
+ ctx.globalAlpha = acao === 'skipped' ? ALFA_DO_PULADO : 1;
79
+ pintar(ctx, { linhas, paleta: coresDoBoneco(boneco.aparencia) }, x, y);
80
+ ctx.globalAlpha = 1;
81
+ if (sinal) pintar(ctx, sinal, x + Math.floor((BONECO - largura(sinal)) / 2), y - altura(sinal) - 1);
82
+ }
83
+
84
+ /**
85
+ * Pinta um quadro inteiro, em pixel lógico: a sala e, do fundo para a frente, mesas e bonecos.
86
+ * Quem está mais abaixo na tela fica na frente: o boneco sentado é pintado antes da própria
87
+ * mesa (o tampo cobre as pernas dele) e quem entrega, parado diante de outra mesa, depois dela.
88
+ * @param {object} ctx o contexto 2D (ou qualquer objeto com `fillStyle`, `globalAlpha` e `fillRect`)
89
+ * @param {object[]} bonecos o que `quadro` devolveu; só há mesa para quem está nesta lista
90
+ */
91
+ export function pintarCena(ctx, bonecos) {
92
+ pintarSala(ctx);
93
+ const camadas = bonecos.flatMap((boneco) => [
94
+ { base: boneco.mesa.retangulo.y + boneco.mesa.retangulo.altura, pintar: pintarMesa, boneco },
95
+ { base: boneco.y + BONECO, pintar: pintarBoneco, boneco },
96
+ ]);
97
+ camadas.sort((a, b) => a.base - b.base);
98
+ for (const camada of camadas) camada.pintar(ctx, camada.boneco);
99
+ }
100
+
101
+ /** O que muda o desenho de um boneco: se nada disto mudou, o quadro é o mesmo. */
102
+ const resumo = (b) => [b.id, b.indice, b.acao, b.pose, b.papel, b.espelhado, b.x, b.y, b.monitor, b.monitorFase].join(':');
103
+
104
+ /**
105
+ * O laço de animação. A cada quadro do navegador pede os bonecos e, se algo mudou desde a última
106
+ * pintura (ou o canvas mudou de tamanho), pinta na escala inteira do canvas, sem suavização.
107
+ * Um quadro que falha não para o laço.
108
+ * @param {object} janela de quem vem o `requestAnimationFrame`
109
+ * @param {object} tela o canvas, já com 320·N × 180·N pixels
110
+ * @param {() => object[] | null} obterQuadro os bonecos deste instante; `null`: ainda não há o que pintar
111
+ * @param {() => void} aoPintar chamada depois de cada pintura
112
+ */
113
+ export function animar(janela, tela, obterQuadro, aoPintar) {
114
+ const ctx = tela.getContext('2d');
115
+ let pintado = '';
116
+ function passo() {
117
+ try {
118
+ const bonecos = obterQuadro();
119
+ const marca = bonecos ? `${tela.width}|${bonecos.map(resumo).join(';')}` : pintado;
120
+ if (marca === pintado) return;
121
+ const n = tela.width / LARGURA;
122
+ ctx.setTransform(n, 0, 0, n, 0, 0);
123
+ ctx.imageSmoothingEnabled = false;
124
+ pintarCena(ctx, bonecos);
125
+ pintado = marca;
126
+ aoPintar();
127
+ } finally {
128
+ janela.requestAnimationFrame(passo);
129
+ }
130
+ }
131
+ janela.requestAnimationFrame(passo);
132
+ }
@@ -0,0 +1,79 @@
1
+ // Demonstração do escritório (regra 23): um roteiro fixo e uma função pura que devolve o estado
2
+ // de cada instante, no formato do `state.json`. O roteiro passa por todos os status de agente,
3
+ // por um checkpoint, por três passagens de bastão, por uma falha e pela execução concluída, e
4
+ // recomeça. A página o desenha pelo mesmo caminho de uma execução de verdade.
5
+ // Puro: sem DOM, sem relógio, sem rede.
6
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 18 e 23 (repositório do OpenCrew).
7
+
8
+ const ELENCO = [
9
+ { id: 'pesquisa', name: 'Pedro Pesquisa', icon: '🔎' },
10
+ { id: 'redacao', name: 'Rita Redação', icon: '✍️' },
11
+ { id: 'design', name: 'Davi Design', icon: '🎨' },
12
+ { id: 'revisao', name: 'Renata Revisão', icon: '🧐' },
13
+ { id: 'social', name: 'Sara Social', icon: '📣' },
14
+ ];
15
+
16
+ const [I, W, C, D, S, F] = ['idle', 'working', 'checkpoint', 'done', 'skipped', 'failed'];
17
+
18
+ // Cada cena: duração, status da execução, passo (`n` e `rotulo`) e o status de cada agente, na
19
+ // ordem do elenco. `passagem` ([de, para, mensagem]) marca a cena em que o bastão troca de mão.
20
+ const CENAS = [
21
+ { ms: 3000, status: 'running', n: 0, rotulo: '', agentes: [I, I, I, I, S] },
22
+ { ms: 6000, status: 'running', n: 1, rotulo: 'Pesquisar o tema', agentes: [W, I, I, I, S] },
23
+ { ms: 5000, status: 'checkpoint', n: 1, rotulo: 'Aprovar a pauta', agentes: [C, I, I, I, S] },
24
+ { ms: 7000, status: 'running', n: 2, rotulo: 'Escrever o texto', agentes: [D, W, I, I, S], passagem: ['pesquisa', 'redacao', 'Pauta aprovada, com três fontes.'] },
25
+ { ms: 7000, status: 'running', n: 3, rotulo: 'Criar a imagem', agentes: [D, D, W, I, S], passagem: ['redacao', 'design', 'Texto pronto, com título e chamada.'] },
26
+ { ms: 5000, status: 'failed', n: 3, rotulo: 'Criar a imagem', agentes: [D, D, F, I, S], motivo: 'Sem acesso ao banco de imagens.' },
27
+ { ms: 6000, status: 'running', n: 3, rotulo: 'Criar a imagem', agentes: [D, D, W, I, S] },
28
+ { ms: 7000, status: 'running', n: 4, rotulo: 'Revisar tudo', agentes: [D, D, D, W, S], passagem: ['design', 'revisao', 'Imagem pronta, no formato do post.'] },
29
+ { ms: 8000, status: 'completed', n: 4, rotulo: 'Revisar tudo', agentes: [D, D, D, D, S] },
30
+ ];
31
+
32
+ /** O roteiro, como dado: o elenco e as cenas, na ordem. */
33
+ export const ROTEIRO = Object.freeze({ crew: 'crew-de-exemplo', total: 4, agentes: ELENCO, cenas: CENAS });
34
+
35
+ const INICIOS = CENAS.map((_, i) => CENAS.slice(0, i).reduce((soma, cena) => soma + cena.ms, 0));
36
+ /** Duração de uma volta do roteiro. */
37
+ export const DURACAO_DEMO_MS = INICIOS.at(-1) + CENAS.at(-1).ms;
38
+ // Data fixa das passagens: só precisa mudar de uma passagem para a outra.
39
+ const BASE = Date.UTC(2026, 0, 5, 9);
40
+
41
+ /** Índice da cena em que cai o instante; o roteiro recomeça ao chegar ao fim. */
42
+ function cenaEm(decorridoMs) {
43
+ const t = Number.isFinite(decorridoMs) && decorridoMs > 0 ? decorridoMs % DURACAO_DEMO_MS : 0;
44
+ return INICIOS.findLastIndex((inicio) => inicio <= t);
45
+ }
46
+
47
+ /** O `label` do agente: o rótulo do último passo em que ele trabalhou, até a cena `ate`. */
48
+ function rotuloDe(agente, ate) {
49
+ const feitas = CENAS.slice(0, ate + 1).filter((cena) => cena.agentes[agente] === W);
50
+ return feitas.length ? feitas.at(-1).rotulo : '';
51
+ }
52
+
53
+ /** A última passagem de bastão até a cena `ate`, no formato do `handoff`. */
54
+ function passagemAte(ate) {
55
+ const cena = CENAS.slice(0, ate + 1).findLastIndex((c) => c.passagem);
56
+ if (cena < 0) return null;
57
+ const [from, to, message] = CENAS[cena].passagem;
58
+ return { from, to, message, completedAt: new Date(BASE + INICIOS[cena]).toISOString() };
59
+ }
60
+
61
+ /**
62
+ * O estado da demonstração num instante. Mesmo instante, mesmo estado; depois de
63
+ * `DURACAO_DEMO_MS` o roteiro recomeça. Vem sem `updatedAt`: a demonstração nunca fica "sem sinal".
64
+ * @param {number} decorridoMs tempo desde que a demonstração começou (inválido vale como 0)
65
+ * @returns {object} um estado no formato do `state.json`
66
+ */
67
+ export function estadoDaDemo(decorridoMs) {
68
+ const indice = cenaEm(decorridoMs);
69
+ const cena = CENAS[indice];
70
+ return {
71
+ crew: ROTEIRO.crew,
72
+ status: cena.status,
73
+ step: { current: cena.n, total: ROTEIRO.total, label: cena.rotulo },
74
+ agents: ELENCO.map((agente, i) => ({ ...agente, status: cena.agentes[i], label: rotuloDe(i, indice) })),
75
+ handoff: passagemAte(indice),
76
+ ...(cena.motivo ? { motivo: cena.motivo } : {}),
77
+ updatedAt: null,
78
+ };
79
+ }
@@ -0,0 +1,27 @@
1
+ // Escala do canvas (regra 16): a tela interna de 320×180 só é ampliada em escala inteira, e a
2
+ // conta é feita em pixels do dispositivo, para o pixel continuar quadrado com a tela do sistema
3
+ // em 125% ou 150%.
4
+ // Puro: sem DOM, sem relógio, sem rede. O tamanho disponível e a razão de pixels entram por
5
+ // parâmetro.
6
+ // Spec: fase-e1-escritorio-ao-vivo.md, regra 16 (repositório do OpenCrew).
7
+ import { ALTURA, LARGURA } from './modelo-mesas.js';
8
+
9
+ // Folga da conta com decimais: um canvas do tamanho exato que esta função devolveu tem de caber
10
+ // de novo (com razão 1,7 e escala 5, a divisão dá 4,999999999999999).
11
+ const FOLGA = 1e-9;
12
+
13
+ /**
14
+ * A maior escala inteira em que a tela cabe no espaço disponível.
15
+ * @param {number} largura largura disponível, em pixels de CSS
16
+ * @param {number} altura altura disponível, em pixels de CSS
17
+ * @param {number} dpr pixels do dispositivo por pixel de CSS; inválido vale como 1
18
+ * @returns {{ n: number, largura: number, altura: number }} `n`: o maior inteiro N ≥ 1 em que
19
+ * 320·N × 180·N cabe em largura·dpr × altura·dpr (se nem N = 1 cabe, 1: a página rola) ·
20
+ * `largura` e `altura`: o tamanho do canvas em pixels de CSS (320·N/dpr × 180·N/dpr)
21
+ */
22
+ export function escala(largura, altura, dpr) {
23
+ const razao = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
24
+ const cabe = Math.min((largura * razao) / LARGURA, (altura * razao) / ALTURA);
25
+ const n = Number.isFinite(cabe) ? Math.max(1, Math.floor(cabe + FOLGA)) : 1;
26
+ return { n, largura: (LARGURA * n) / razao, altura: (ALTURA * n) / razao };
27
+ }