SLOPSHOPPER

painel-orquestracao

Status de contexto/custo, delegações em andamento (scripts/agentes) e sentinela das cópias CLAUDE.md/AGENTS.md e .claude/.agents skills.

newpanebandguardcommandtoast
★ 1v0.1.0no licenseupdated 2026-10-09mthhenri/ContratadosRPG/.claude/mods/painel-orquestracao
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · painel-orquestracao
│ ┃ Orquestração ✕ › fix the failing auth test and add an audit log call │ ┃ Sessão │ ┃ Contexto: 49% · Custo: US$ 0.42 ⏺ Read(src/auth.ts) │ ┃ Cópias sincronizadas ⎿ Read 6 lines │ ┃ idênticas ⏺ Update(src/auth.ts) │ ┃ Delegações em andamento ⎿ Added 2 lines, removed 1 line │ ┃ nenhuma ⏺ Bash(bun test) │ ┃ Últimas delegações ⎿ 3 pass, 1 fail │ ┃ nenhuma registrada em │ ┃ .agentes/delegacoes.log ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /painel │ ⎿ painel-orquestracao: Contexto: 49% · Custo: US$ 0.42 │ ⎿ painel-orquestracao: Cópias CLAUDE.md/AGENTS.md e skills: idênti │ ⎿ painel-orquestracao: Delegações em andamento: nenhuma │ ⎿ painel-orquestracao: Últimas delegações: │ ⎿ painel-orquestracao: nenhuma registrada em .agentes/delegacoes │ │ ctx 49% · US$ 0.42 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ painel-orquestracao: ctx 49% · US$ 0.42

Draws

Band
ctx 49% · US$ 0.42
Pane · Orquestração
Sessão Contexto: 49% · Custo: US$ 0.42 Cópias sincronizadas idênticas Delegações em andamento nenhuma Últimas delegações nenhuma registrada em .agentes/delegacoes.log
README

contratados-rpg

Site completo do RPG Contratados (SCP Foundation RPG v4): calculadora de stats, campanhas, fichas de jogador/criatura/NPC com atualização em tempo real e guia de missão.

Sucessor da contratados-calculadora (arquivada após o milestone M1).

Documentação

ArquivoPapel
docs/SYSTEM.SPEC.mdConstituição do projeto — precede tudo
docs/CONVENTIONS.mdReferência rápida de convenções de código
docs/design/DESIGN.mdIdentidade visual, tokens e biblioteca de componentes (frontend/src/app/shared/ui/)
docs/context/CONTEXT.mdEstado atual e próxima task
docs/context/HISTORY.mdHistórico completo — o que aconteceu e por quê
docs/context/PROBLEMS.mdProblemas conhecidos do sistema
docs/context/MEMORY.mdMapa: onde fica o quê e onde estão as regras
docs/context/IDEAS.mdIdeias levantadas que ainda não viraram spec
docs/SCHEMA.mdSchema SQL alvo + forma dos documentos JSONB
docs/DEPLOY.mdRunbook de deploy em produção — ainda descreve o Render; reescrita para Cloud Run pendente (ver docs/context/CONTEXT.md)
docs/DEVELOPMENT.mdBanco local reproduzível, fixtures e credenciais de desenvolvimento
docs/PARIDADE-M1.mdVerificação de paridade da calculadora (fecha o M1)
docs/core/sistema-v4.1.3.mdFonte da verdade das regras do jogo
docs/core/guia_de_mestre-v4.2.0.mdFonte da verdade de criação de ameaças
docs/specs/Workflow spec-driven: backlog → active → done

API REST (Swagger)

Com o backend em execução, a documentação navegável da API REST está em /api/docs e o documento OpenAPI bruto em /api/docs-json. As duas rotas são públicas e não usam o envelope StandardResponse das operações de negócio.

O Swagger cobre somente HTTP REST. O Socket.IO continua broadcast-only: mutações entram pela API REST e os eventos permanecem documentados no gateway, fora do OpenAPI. Ao criar, remover ou alterar endpoint ou DTO público, atualize a documentação na mesma task executando npm run openapi:gerar-contratos --workspace=backend e revise o teste de cobertura OpenAPI.

Arquitetura

Monorepo npm workspaces:

  • shared/ (@contratados-rpg/shared) — DTOs, enums, interfaces, validators e o motor de regras do jogo (regras/)
  • backend/ — NestJS + Knex (SQL bruto) + Socket.IO (broadcast-only) → Google Cloud Run
  • frontend/ — Angular 21 (standalone + Signals), biblioteca de componentes própria em frontend/src/app/shared/ui/ (sem PrimeNG desde ui-05, ver docs/design/DESIGN.md) → Cloudflare
  • Banco — PostgreSQL 16 (local: Docker; produção: Supabase)

Desenvolvimento

npm install                              # instala os 3 workspaces
npm run db:up                            # Postgres via Docker Compose
npm run db:migrate --workspace=backend   # migrations
npm run db:reset:dev                     # apaga/recria o banco local e aplica fixtures
npm run dev                              # backend + frontend juntos (predev sobe o banco)

dev roda backend:dev (API em http://localhost:3100) e frontend:dev (SPA em http://localhost:4300) em paralelo; rode-os separados se preferir consoles isolados.

O reset é destrutivo e exclusivo do ambiente local. Consulte docs/DEVELOPMENT.md antes do primeiro uso.

Variáveis de ambiente (backend)

DB_HOST  DB_PORT  DB_NOME  DB_USUARIO  DB_SENHA
JWT_SECRETO  JWT_EXPIRACAO
APP_PORTA  APP_AMBIENTE  APP_FRONTEND_ORIGEM

Deploy (produção)

Deploy por integração nativa das plataformas: no push para master, um trigger do Cloud Build compila a imagem, roda as migrations e reimplanta o backend no Google Cloud Run, enquanto a Cloudflare Pages puxa do Git e reimplanta o frontend sozinha (banco no Supabase) — sem GitHub Actions no deploy. O backend migrou do Render para o Cloud Run em 2026-09-01 (ver docs/context/HISTORY.md); o runbook docs/DEPLOY.md ainda descreve o fluxo antigo do Render e está pendente de reescrita.

Source 2 files
hooks/register.tsx 171 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Delegacao, Painel } from '../types'
5
6const PAINEL = 'painel-orquestracao'
7const INTERVALO_MS = 5000
8const RECENTE_MS = 10 * 60 * 1000
9
10// Pares que o CLAUDE.md exige idênticos ("Sincronização com CLAUDE.md").
11const PARES = [
12  ['CLAUDE.md', 'AGENTS.md'],
13  ['.claude/skills', '.agents/skills'],
14] as const
15const SINCRONIZADO = /(^|[\\/])(CLAUDE\.md|AGENTS\.md)$|[\\/]\.(claude|agents)[\\/]skills[\\/]/
16
17const VAZIO: Painel = { emAndamento: [], ultimas: [], divergencias: [], contexto: null, custo: null }
18const estado = atom({ plugin: 'painel-orquestracao', key: 'estado' } as const, VAZIO)
19
20// Linha TSV de scripts/agentes/_comum.sh (registrar): data destino modo modelo esforço s status hash pasta
21export function lerLinha(linha: string): Delegacao | null {
22  const c = linha.split('\t')
23  if (c.length < 7) return null
24  const [quando = '', destino = '', modo = '', modelo = '', esforco = '', segundos = '0', status = '0'] = c
25  return { quando, destino, modo, modelo, esforco, segundos: Number(segundos), status: Number(status) }
26}
27
28export function resumir(d: Delegacao): string {
29  const ok = d.status === 0 ? 'ok' : `falhou(${d.status})`
30  return `${d.destino} ${d.modo} · ${d.modelo} · ${d.esforco} · ${d.segundos}s · ${ok}`
31}
32
33async function lerDelegacoes($: any): Promise<Delegacao[]> {
34  const texto: string = await $.fs.read('.agentes/delegacoes.log').catch(() => '')
35  return texto.split('\n').map(lerLinha).filter((d): d is Delegacao => d !== null).slice(-8)
36}
37
38async function lerVagas($: any): Promise<string[]> {
39  const itens: { name: string; kind: string }[] = await $.fs.list('.agentes/vagas').catch(() => [])
40  return itens.filter(i => i.kind === 'dir').map(i => i.name.split('.')[0] ?? i.name)
41}
42
43// Depois de um Edit/Write bem-sucedido numa das cópias, revalida os pares; true se foi uma delas.
44async function aposEditar($: any, caminho: string, ran: { deny?: unknown; isError?: boolean }): Promise<boolean> {
45  if (ran.deny !== undefined || ran.isError === true || !SINCRONIZADO.test(caminho)) return false
46  await atualizar($, true)
47  return true
48}
49
50// `git diff --no-index` compara arquivos e pastas; saída 1 = diferem.
51async function verificarCopias($: any): Promise<string[]> {
52  const divergem: string[] = []
53  for (const [a, b] of PARES) {
54    const r = await $.process.run(['git', 'diff', '--no-index', '--quiet', a, b]).catch(() => null)
55    if (r !== null && r.exitCode === 1) divergem.push(`${a} ≠ ${b}`)
56  }
57  return divergem
58}
59
60function textoStatus(p: Painel): string | undefined {
61  const partes: string[] = []
62  if (p.contexto !== null) partes.push(`ctx ${Math.round(p.contexto)}%`)
63  if (p.custo !== null) partes.push(`US$ ${p.custo.toFixed(2)}`)
64  if (p.emAndamento.length > 0) partes.push(`deleg ${p.emAndamento.join('+')}`)
65  if (p.divergencias.length > 0) partes.push('⚠ cópias divergem')
66  return partes.length > 0 ? partes.join(' · ') : undefined
67}
68
69// Resumo em texto puro, para o chat de qualquer interface (o painel lateral pode não existir nela).
70export function relatorio(p: Painel): string {
71  const linhas = [
72    `Contexto: ${p.contexto === null ? '—' : `${Math.round(p.contexto)}%`} · Custo: ${p.custo === null ? '—' : `US$ ${p.custo.toFixed(2)}`}`,
73    `Cópias CLAUDE.md/AGENTS.md e skills: ${p.divergencias.length === 0 ? 'idênticas' : `DIVERGEM — ${p.divergencias.join('; ')}`}`,
74    `Delegações em andamento: ${p.emAndamento.length === 0 ? 'nenhuma' : p.emAndamento.join(', ')}`,
75    'Últimas delegações:',
76    ...(p.ultimas.length === 0 ? ['  nenhuma registrada em .agentes/delegacoes.log'] : [...p.ultimas].reverse().map(d => `  ${resumir(d)}`)),
77  ]
78  return linhas.join('\n')
79}
80
81// Atualiza o que é barato (arquivos e uso da sessão); a checagem de cópias é separada.
82async function atualizar($: any, verificar: boolean) {
83  const uso = await $.session.usage().catch(() => null)
84  const [ultimas, emAndamento] = await Promise.all([lerDelegacoes($), lerVagas($)])
85  const divergencias = verificar ? await verificarCopias($) : (await read($, estado)).divergencias
86  const novo: Painel = {
87    emAndamento,
88    ultimas,
89    divergencias,
90    contexto: uso?.context?.percent ?? null,
91    custo: uso?.cost?.usd ?? null,
92  }
93  await update($, estado, () => novo)
94  $.ui.status(textoStatus(novo))
95}
96
97export const register: Register = on => {
98  let tocouCopias = false
99
100  on('session.start', async ($, e, next) => {
101    await $.command.register({ name: 'painel', description: 'Abre o painel de delegações, contexto e cópias sincronizadas' })
102    void atualizar($, true)
103    $.clock.every(INTERVALO_MS, () => atualizar($, false))
104    return next(e)
105  })
106
107  on('command.run', { command: 'painel' }, async $ => {
108    await atualizar($, true)
109    await $.ui.open({ id: PAINEL, title: 'Orquestração' })
110    return { text: relatorio(await read($, estado)) }
111  })
112
113  on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
114    const ran = await next(e)
115    if (await aposEditar($, e.file_path, ran)) tocouCopias = true
116    return ran
117  })
118
119  on('tool.call', { tool: 'Write' }, async ($, e, next) => {
120    const ran = await next(e)
121    if (await aposEditar($, e.file_path, ran)) tocouCopias = true
122    return ran
123  })
124
125  // Entre editar uma cópia e a outra é normal divergir; só avisa se o turno terminou assim.
126  on('turn.complete', async ($, e, next) => {
127    if (tocouCopias) {
128      tocouCopias = false
129      await atualizar($, true)
130      const { divergencias } = await read($, estado)
131      if (divergencias.length > 0) $.ui.toast(`Cópias divergem: ${divergencias.join('; ')}`)
132    }
133    return next(e)
134  })
135
136  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
137    const p = await read($, estado)
138    const agora = await $.clock.now()
139    const ultima = p.ultimas[p.ultimas.length - 1]
140    const recente = ultima !== undefined && agora - Date.parse(ultima.quando) < RECENTE_MS
141    if (e.props.hasSurvey) return next(e)
142    const { Box, Text } = $.ui.resolve(e)
143    return (
144      <Box flexDirection="column">
145        {p.divergencias.length > 0 && <Text color="red">⚠ Cópias divergem: {p.divergencias.join('; ')}</Text>}
146        {p.emAndamento.length > 0 && <Text>Delegando agora: {p.emAndamento.join(', ')}</Text>}
147        {p.emAndamento.length === 0 && recente && ultima !== undefined && <Text dimColor>Última delegação: {resumir(ultima)}</Text>}
148        <Text dimColor>{textoStatus(p) ?? 'painel-orquestracao ativo'}</Text>
149      </Box>
150    )
151  })
152
153  on('ui.render', { component: 'Pane', requestId: PAINEL }, async ($, e) => {
154    const p = await read($, estado)
155    const { Box, Text } = $.ui.resolve(e)
156    return (
157      <Box flexDirection="column">
158        <Text bold>Sessão</Text>
159        <Text>Contexto: {p.contexto === null ? '—' : `${Math.round(p.contexto)}%`} · Custo: {p.custo === null ? '—' : `US$ ${p.custo.toFixed(2)}`}</Text>
160        <Text bold>Cópias sincronizadas</Text>
161        {p.divergencias.length === 0 ? <Text dimColor>idênticas</Text> : p.divergencias.map(d => <Text color="red">{d}</Text>)}
162        <Text bold>Delegações em andamento</Text>
163        {p.emAndamento.length === 0 ? <Text dimColor>nenhuma</Text> : <Text>{p.emAndamento.join(', ')}</Text>}
164        <Text bold>Últimas delegações</Text>
165        {p.ultimas.length === 0 && <Text dimColor>nenhuma registrada em .agentes/delegacoes.log</Text>}
166        {[...p.ultimas].reverse().map(d => <Text dimColor={d.status === 0}>{resumir(d)}</Text>)}
167      </Box>
168    )
169  })
170}
171
types/index.d.ts 29 lines
1export type Delegacao = {
2  quando: string
3  destino: string
4  modo: string
5  modelo: string
6  esforco: string
7  segundos: number
8  status: number
9}
10
11export type Painel = {
12  /** Vagas ocupadas em .agentes/vagas, ex.: "codex" (uma entrada por vaga). */
13  emAndamento: string[]
14  /** Últimas linhas de .agentes/delegacoes.log, da mais antiga para a mais nova. */
15  ultimas: Delegacao[]
16  /** Pares de cópias idênticas que divergem, ex.: "CLAUDE.md ≠ AGENTS.md". */
17  divergencias: string[]
18  /** Preenchimento da janela de contexto (0–100), se conhecido. */
19  contexto: number | null
20  /** Custo da sessão em USD, se conhecido. */
21  custo: number | null
22}
23
24declare module 'claude-code' {
25  interface PluginState {
26    'painel-orquestracao': { estado: Painel }
27  }
28}
29